From cc35654388ae2f14c1bc76453ebd9dc578fed301 Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Wed, 5 Jul 2023 22:26:39 +1000 Subject: [PATCH] Converting from tide to axum (#1797) * Starting to chase down testing * commenting out unused/inactive endpoints, adding more tests * clippyism * making clippy happy v2 * testing when things are not right * moar checkpoint * splitting up testkit things a bit * moving https -> tide * mad lad be crabbin * spawning like a frog * something something different spawning * woot it works ish * more server things * adding version header to requests * adding kopid_middleware * well that was supposed to be an hour... four later * more nonsense * carrying on with the conversion * first pass through the conversion is DONE! * less pub more better * session storage works better, fixed some paths * axum-csp version thing * try a typedheader * better openssl config things * updating lockfile * http2 * actually sending JSON when we say we will! * just about to do something dumb * flargl * more yak shaving * So many clippy-isms, fixing up a query handler bleep bloop * So many clippy-isms, fixing up a query handler bleep bloop * fmt * all tests pass including basic web logins and nav * so much clippyism * stripping out old comments * fmt * commenty things * stripping out tide * updates * de-tiding things * fmt * adding optional header matching ,thanks @cuberoot74088 * oauth2 stuff to match #1807 but in axum * CLIPPY IS FINALLY SATED * moving scim from /v1/scim to /scim * one day clippy will make sense * cleanups * removing sketching middleware * cleanup, strip a broken test endpoint (routemap), more clippy * docs fmt * pulling axum-csp from the wrong cargo.toml * docs fmt * fmt fixes --- Cargo.lock | 986 +++++---- Cargo.toml | 20 +- Makefile | 31 +- book/src/DEVELOPER_README.md | 45 +- .../designs/content_security_policy.md | 7 + libs/client/src/lib.rs | 49 +- libs/file_permissions/src/lib.rs | 14 + libs/sketching/Cargo.toml | 1 - libs/sketching/src/lib.rs | 1 - libs/sketching/src/middleware.rs | 107 - scripts/setup_dev_environment.sh | 4 +- scripts/test_coverage.sh | 64 + server/core/Cargo.toml | 18 +- server/core/src/actors/v1_read.rs | 1 + server/core/src/actors/v1_write.rs | 18 +- server/core/src/config.rs | 28 +- server/core/src/https/generic.rs | 33 + server/core/src/https/javascript.rs | 57 + server/core/src/https/manifest.rs | 80 +- server/core/src/https/middleware.rs | 229 -- server/core/src/https/middleware/caching.rs | 20 + .../core/src/https/middleware/compression.rs | 25 + .../core/src/https/middleware/csp_headers.rs | 21 + server/core/src/https/middleware/mod.rs | 89 + server/core/src/https/mod.rs | 1290 ++++------- server/core/src/https/oauth2.rs | 1024 ++++----- server/core/src/https/routemaps.rs | 94 - server/core/src/https/tests.rs | 23 + server/core/src/https/ui.rs | 65 + server/core/src/https/v1.rs | 1927 ++++++++++------- server/core/src/https/v1_scim.rs | 449 ++-- server/core/src/lib.rs | 27 +- server/daemon/run_insecure_dev_server.sh | 2 + server/daemon/src/main.rs | 50 +- server/lib/Cargo.toml | 1 - server/lib/src/idm/credupdatesession.rs | 10 +- server/lib/src/idm/oauth2.rs | 1 + server/lib/src/plugins/namehistory.rs | 2 +- server/lib/src/schema.rs | 2 +- server/lib/src/server/migrations.rs | 2 +- server/testkit/Cargo.toml | 2 +- server/testkit/src/lib.rs | 33 +- server/testkit/tests/default_entries.rs | 129 +- server/testkit/tests/http_manifest.rs | 22 + server/testkit/tests/https_middleware.rs | 19 +- server/testkit/tests/oauth2_test.rs | 118 +- server/testkit/tests/person.rs | 32 + server/testkit/tests/routes.rs | 681 ++++++ server/testkit/tests/scim_test.rs | 35 + server/testkit/tests/self.rs | 51 + server/testkit/tests/service_account.rs | 30 + server/testkit/tests/system.rs | 29 + tools/iam_migrations/freeipa/src/main.rs | 8 +- tools/iam_migrations/ldap/src/main.rs | 10 +- unix_integration/pam_kanidm/src/pam/module.rs | 4 + unix_integration/src/daemon.rs | 14 +- unix_integration/src/selinux_util.rs | 2 +- unix_integration/src/tasks_daemon.rs | 7 + unix_integration/src/unix_config.rs | 15 +- unix_integration/src/unix_passwd.rs | 15 +- unix_integration/tests/cache_layer_test.rs | 15 +- 61 files changed, 4704 insertions(+), 3484 deletions(-) create mode 100644 book/src/developers/designs/content_security_policy.md delete mode 100644 libs/sketching/src/middleware.rs create mode 100755 scripts/test_coverage.sh create mode 100644 server/core/src/https/generic.rs create mode 100644 server/core/src/https/javascript.rs delete mode 100644 server/core/src/https/middleware.rs create mode 100644 server/core/src/https/middleware/caching.rs create mode 100644 server/core/src/https/middleware/compression.rs create mode 100644 server/core/src/https/middleware/csp_headers.rs create mode 100644 server/core/src/https/middleware/mod.rs delete mode 100644 server/core/src/https/routemaps.rs create mode 100644 server/core/src/https/tests.rs create mode 100644 server/core/src/https/ui.rs create mode 100644 server/testkit/tests/http_manifest.rs create mode 100644 server/testkit/tests/person.rs create mode 100644 server/testkit/tests/routes.rs create mode 100644 server/testkit/tests/self.rs create mode 100644 server/testkit/tests/service_account.rs create mode 100644 server/testkit/tests/system.rs diff --git a/Cargo.lock b/Cargo.lock index 9a44e4581..6e501ed50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,9 +222,11 @@ checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" dependencies = [ "flate2", "futures-core", - "futures-io", "memchr", - "pin-project-lite 0.2.9", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", ] [[package]] @@ -236,20 +238,10 @@ dependencies = [ "flate2", "futures-core", "memchr", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", ] -[[package]] -name = "async-dup" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7427a12b8dc09291528cfb1da2447059adb4a257388c2acd6497a79d55cf6f7c" -dependencies = [ - "futures-io", - "simple-mutex", -] - [[package]] name = "async-executor" version = "1.5.1" @@ -279,22 +271,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "async-h1" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8101020758a4fc3a7c326cb42aa99e9fa77cbfb76987c128ad956406fe1f70a7" -dependencies = [ - "async-channel", - "async-dup", - "async-std", - "futures-core", - "http-types", - "httparse", - "log", - "pin-project", -] - [[package]] name = "async-io" version = "1.13.0" @@ -309,7 +285,7 @@ dependencies = [ "log", "parking", "polling", - "rustix", + "rustix 0.37.22", "slab", "socket2", "waker-fn", @@ -324,24 +300,6 @@ dependencies = [ "event-listener", ] -[[package]] -name = "async-process" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" -dependencies = [ - "async-io", - "async-lock", - "autocfg", - "blocking", - "cfg-if 1.0.0", - "event-listener", - "futures-lite", - "rustix", - "signal-hook", - "windows-sys 0.48.0", -] - [[package]] name = "async-recursion" version = "1.0.4" @@ -355,39 +313,25 @@ dependencies = [ [[package]] name = "async-session" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f" +checksum = "07da4ce523b4e2ebaaf330746761df23a465b951a83d84bbce4233dabedae630" dependencies = [ "anyhow", - "async-std", + "async-lock", "async-trait", - "base64 0.12.3", + "base64 0.13.1", "bincode", "blake3", "chrono", - "hmac 0.8.1", - "kv-log-macro", - "rand 0.7.3", + "hmac 0.11.0", + "log", + "rand 0.8.5", "serde", "serde_json", "sha2 0.9.9", ] -[[package]] -name = "async-sse" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10" -dependencies = [ - "async-channel", - "async-std", - "http-types", - "log", - "memchr", - "pin-project-lite 0.1.12", -] - [[package]] name = "async-std" version = "1.12.0" @@ -398,7 +342,6 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", - "async-process", "crossbeam-utils", "futures-channel", "futures-core", @@ -409,25 +352,12 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.9", + "pin-project-lite", "pin-utils", "slab", "wasm-bindgen-futures", ] -[[package]] -name = "async-std-openssl" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "408a76b00fc49b11fe78f1f7a90557a3c887af1d4570fb33e15a70eb7e6b95ee" -dependencies = [ - "async-dup", - "async-std", - "futures-util", - "openssl", - "openssl-sys", -] - [[package]] name = "async-task" version = "4.4.0" @@ -436,9 +366,9 @@ checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7" dependencies = [ "proc-macro2", "quote", @@ -496,6 +426,152 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bitflags 1.3.2", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-auth" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620b37645b77baab8160f93421568d7b3dd25da0a160fab38eb1c4ef611f6d98" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.13.1", + "http", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-csp" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4faf3873ea8d6828e5705070e10fdf4f61420d22523835a2a140ae4b2e8d2526" +dependencies = [ + "axum", + "http", + "regex", + "tokio", +] + +[[package]] +name = "axum-extra" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "febf23ab04509bd7672e6abe76bd8277af31b679e89fa5ffc6087dc289a448a3" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie 0.17.0", + "futures-util", + "http", + "http-body", + "mime", + "pin-project-lite", + "serde", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb524613be645939e280b7279f7b017f98cf7f5ef084ec374df373530e73277" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "axum-server" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447f28c85900215cc1bea282f32d4a2f22d55c5a300afdfbc661c8d6a632e063" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "openssl", + "pin-project-lite", + "tokio", + "tokio-openssl", + "tower-service", +] + +[[package]] +name = "axum-sessions" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714cad544cd87d8da821cda715bb9aaa5d4d1adbdb64c549b18138e3cbf93c44" +dependencies = [ + "async-session", + "axum", + "axum-extra", + "futures", + "http-body", + "tokio", + "tower", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.68" @@ -580,7 +656,7 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "prettyplease 0.2.6", + "prettyplease 0.2.9", "proc-macro2", "quote", "regex", @@ -619,9 +695,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "blake2" @@ -852,7 +928,7 @@ dependencies = [ "bitflags 1.3.2", "clap_derive", "clap_lex", - "indexmap", + "indexmap 1.9.3", "once_cell", "strsim", "termcolor", @@ -915,9 +991,9 @@ dependencies = [ [[package]] name = "concread" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d825450e64aece76bdcf5c6d115c454ebb284c892da3e4cc7ff1e62e72069" +checksum = "f8c43d983bcbf6cbc1b24f9d9a6a6474d762c8744920984d2e0f4e93c2c3e9fa" dependencies = [ "ahash 0.7.6", "crossbeam-epoch", @@ -1003,16 +1079,33 @@ dependencies = [ ] [[package]] -name = "cookie_store" -version = "0.16.1" +name = "cookie" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e4b6aa369f41f5faa04bb80c9b1f4216ea81646ed6124d76ba5c49a7aafd9cd" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "base64 0.21.2", + "hmac 0.12.1", + "percent-encoding", + "rand 0.8.5", + "sha2 0.10.7", + "subtle", + "time 0.3.22", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa" dependencies = [ "cookie 0.16.2", "idna 0.2.3", "log", "publicsuffix", "serde", + "serde_derive", "serde_json", "time 0.3.22", "url", @@ -1202,6 +1295,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array 0.14.7", + "subtle", +] + [[package]] name = "csv" version = "1.2.2" @@ -1482,13 +1585,10 @@ dependencies = [ ] [[package]] -name = "erased-serde" -version = "0.3.25" +name = "equivalent" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569" -dependencies = [ - "serde", -] +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" [[package]] name = "errno" @@ -1554,22 +1654,6 @@ dependencies = [ "instant", ] -[[package]] -name = "femme" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc04871e5ae3aa2952d552dae6b291b3099723bf779a8054281c1366a54613ef" -dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "fernet" version = "0.2.1" @@ -1733,7 +1817,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.9", + "pin-project-lite", "waker-fn", ] @@ -1773,7 +1857,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.9", + "pin-project-lite", "pin-utils", "slab", ] @@ -1925,14 +2009,14 @@ dependencies = [ [[package]] name = "gloo-history" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd451019e0b7a2b8a7a7b23e74916601abf1135c54664e57ff71dcc26dfcdeb7" +checksum = "ddfd137a4b629e72b8c949ec56c71ea9bd5491cc66358a0a7787e94875feec71" dependencies = [ "gloo-events", "gloo-utils", "serde", - "serde-wasm-bindgen", + "serde-wasm-bindgen 0.5.0", "serde_urlencoded", "thiserror", "wasm-bindgen", @@ -1999,9 +2083,9 @@ dependencies = [ [[package]] name = "gloo-utils" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" dependencies = [ "js-sys", "serde", @@ -2029,9 +2113,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -2039,7 +2123,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -2081,6 +2165,31 @@ dependencies = [ "hashbrown 0.14.0", ] +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1 0.10.5", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -2096,15 +2205,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.1" @@ -2127,16 +2227,6 @@ dependencies = [ "hmac 0.10.1", ] -[[package]] -name = "hmac" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" -dependencies = [ - "crypto-mac 0.8.0", - "digest 0.9.0", -] - [[package]] name = "hmac" version = "0.10.1" @@ -2147,6 +2237,25 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hostname-validator" version = "1.1.1" @@ -2172,20 +2281,14 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", - "pin-project-lite 0.2.9", + "pin-project-lite", ] [[package]] -name = "http-client" -version = "6.5.3" +name = "http-range-header" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5" -dependencies = [ - "async-trait", - "cfg-if 1.0.0", - "http-types", - "log", -] +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] name = "http-types" @@ -2200,7 +2303,7 @@ dependencies = [ "cookie 0.14.4", "futures-lite", "infer", - "pin-project-lite 0.2.9", + "pin-project-lite", "rand 0.7.3", "serde", "serde_json", @@ -2223,9 +2326,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -2237,7 +2340,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite 0.2.9", + "pin-project-lite", "socket2", "tokio", "tower-service", @@ -2349,7 +2452,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40fc102e70475c320b185cd18c1e48bba2d7210b63970a4d581ef903e4368ef7" dependencies = [ - "indexmap", + "indexmap 1.9.3", ] [[package]] @@ -2362,6 +2465,16 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "infer" version = "0.2.3" @@ -2410,19 +2523,18 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.7.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" dependencies = [ "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", + "rustix 0.38.2", "windows-sys 0.48.0", ] @@ -2437,9 +2549,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" [[package]] name = "jobserver" @@ -2638,11 +2750,20 @@ name = "kanidmd_core" version = "1.1.0-beta.13-dev" dependencies = [ "async-trait", + "axum", + "axum-auth", + "axum-csp", + "axum-macros", + "axum-server", + "axum-sessions", "chrono", "compact_jwt", "cron", "futures-util", + "http", "http-types", + "hyper", + "hyper-tls", "kanidm_proto", "kanidmd_lib", "ldap3_proto", @@ -2651,18 +2772,19 @@ dependencies = [ "profiles", "rand 0.8.5", "regex", + "reqwest", "serde", "serde_json", "sketching", - "tide", - "tide-compress", - "tide-openssl", "time 0.3.22", "tokio", "tokio-openssl", "tokio-util", "toml", + "tower", + "tower-http", "tracing", + "tracing-subscriber", "urlencoding", "uuid", ] @@ -2706,7 +2828,6 @@ dependencies = [ "smartstring", "smolset", "sshkeys", - "tide", "time 0.3.22", "tokio", "tokio-util", @@ -2766,7 +2887,7 @@ dependencies = [ "kanidm_proto", "qrcode", "serde", - "serde-wasm-bindgen", + "serde-wasm-bindgen 0.4.5", "serde_json", "time 0.3.22", "url", @@ -2979,6 +3100,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + [[package]] name = "lock_api" version = "0.4.10" @@ -2995,7 +3122,6 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" dependencies = [ - "serde", "value-bag", ] @@ -3032,6 +3158,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + [[package]] name = "mathru" version = "0.13.0" @@ -3082,6 +3214,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3185,7 +3327,7 @@ dependencies = [ "lazy_static", "libc", "libnss", - "paste 1.0.12", + "paste 1.0.13", ] [[package]] @@ -3263,11 +3405,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi 0.3.1", "libc", ] @@ -3490,7 +3632,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -3516,9 +3658,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" [[package]] name = "paste-impl" @@ -3570,9 +3712,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16833386b02953ca926d19f64af613b9bf742c48dcd5e09b32fbfc9740bf84e2" +checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" dependencies = [ "thiserror", "ucd-trie", @@ -3615,18 +3757,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", @@ -3635,15 +3777,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.12" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" [[package]] name = "pin-utils" @@ -3670,9 +3806,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plotters" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" dependencies = [ "num-traits", "plotters-backend", @@ -3683,15 +3819,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] @@ -3708,7 +3844,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.9", + "pin-project-lite", "windows-sys 0.48.0", ] @@ -3741,9 +3877,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" +checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282" dependencies = [ "proc-macro2", "syn 2.0.23", @@ -4051,7 +4187,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.9", + "pin-project-lite", "serde", "serde_json", "serde_urlencoded", @@ -4066,12 +4202,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "route-recognizer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e" - [[package]] name = "route-recognizer" version = "0.3.1" @@ -4170,29 +4300,42 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.20" +version = "0.37.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +checksum = "8818fa822adcc98b18fedbb3632a6a33213c070556b5aa7c4c8cc21cff565c4c" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabcb0461ebd01d6b79945797c27f8529082226cb630a9865a71870ff63532a4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", "windows-sys 0.48.0", ] [[package]] name = "rustversion" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" [[package]] name = "same-file" @@ -4205,11 +4348,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] @@ -4270,7 +4413,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b5ff8f655381fc80f470384da0a461b19b7bf18d685639ccf6df0abd3d2363d" dependencies = [ - "bitflags 2.3.2", + "bitflags 2.3.3", "libc", "once_cell", "reference-counted-singleton", @@ -4325,9 +4468,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.164" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" dependencies = [ "serde_derive", ] @@ -4344,10 +4487,21 @@ dependencies = [ ] [[package]] -name = "serde_bytes" -version = "0.11.9" +name = "serde-wasm-bindgen" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c5113243e4a3a1c96587342d067f3e6b0f50790b6cf40d2868eb647a3eef0e" dependencies = [ "serde", ] @@ -4374,24 +4528,15 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.164" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" dependencies = [ "proc-macro2", "quote", "syn 2.0.23", ] -[[package]] -name = "serde_fmt" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" -dependencies = [ - "serde", -] - [[package]] name = "serde_json" version = "1.0.99" @@ -4405,10 +4550,11 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +checksum = "0b1b6471d7496b051e03f1958802a73f88b947866f5146f329e47e36554f4e55" dependencies = [ + "itoa", "serde", ] @@ -4444,6 +4590,17 @@ dependencies = [ "sha1_smol", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha1_smol" version = "1.0.0" @@ -4516,16 +4673,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" -[[package]] -name = "signal-hook" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -4535,22 +4682,12 @@ dependencies = [ "libc", ] -[[package]] -name = "simple-mutex" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38aabbeafa6f6dead8cebf246fe9fae1f9215c8d29b3a69f93bd62a9e4a3dcd6" -dependencies = [ - "event-listener", -] - [[package]] name = "sketching" version = "1.1.0-beta.13-dev" dependencies = [ "async-trait", "num_enum", - "tide", "tracing", "tracing-forest", "tracing-subscriber", @@ -4682,7 +4819,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha1", + "sha1 0.6.1", "syn 1.0.109", ] @@ -4704,74 +4841,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" -[[package]] -name = "sval" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2faba619276044eec7cd160d87b15d9191fb9b9f7198440343d2144f760cf08" - -[[package]] -name = "sval_buffer" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a353d3cca10721384077c9643c3fafdd6ed2600e57933b8e45c0b580d97b25af" -dependencies = [ - "sval", - "sval_ref", -] - -[[package]] -name = "sval_dynamic" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee5fc7349e9f6cb2ab950046818f66ad3f2d7209ccc5dced93da19292a30273a" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_fmt" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "098fb51d5d6007bd2c3f0a23b79aa953d7c46bf943086ce51424c3187c40f9b1" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_json" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f01126a2783d767496f18f13af26ab2587881f6343368bb26dc62956a723d1c7" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_ref" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5854d9eaa7bd31840a850322591c59c5b547eb29c9a6ecee1989d6ef963312ce" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_serde" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cdd25fc04c5e882787d62112591aa93efb5bdc2000b43164d29f08582bb85f7" -dependencies = [ - "serde", - "sval", - "sval_buffer", - "sval_fmt", -] - [[package]] name = "syn" version = "1.0.109" @@ -4794,6 +4863,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "synstructure" version = "0.12.6" @@ -4808,9 +4883,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" +checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac" [[package]] name = "tempfile" @@ -4822,7 +4897,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand", "redox_syscall 0.3.5", - "rustix", + "rustix 0.37.22", "windows-sys 0.48.0", ] @@ -4880,58 +4955,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "tide" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0" -dependencies = [ - "async-h1", - "async-session", - "async-sse", - "async-std", - "async-trait", - "femme", - "futures-util", - "http-client", - "http-types", - "kv-log-macro", - "log", - "pin-project-lite 0.2.9", - "route-recognizer 0.2.0", - "serde", - "serde_json", -] - -[[package]] -name = "tide-compress" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a55e754f247bb04c6ea1c2ec46f1a4e8a91dabca9dc7a38c67aa3a9df6b359" -dependencies = [ - "async-compression 0.3.15", - "futures-lite", - "http-types", - "regex", - "tide", -] - -[[package]] -name = "tide-openssl" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca37203863763d3faf05b22d32a0c2da7a2d429b8fb22345e19e45ec2ad1071" -dependencies = [ - "async-dup", - "async-h1", - "async-std", - "async-std-openssl", - "futures-util", - "openssl", - "openssl-sys", - "tide", -] - [[package]] name = "tikv-jemalloc-sys" version = "0.5.3+5.3.0-patched" @@ -5067,7 +5090,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "pin-project-lite 0.2.9", + "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", @@ -5114,7 +5137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", ] @@ -5127,7 +5150,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", "tracing", ] @@ -5143,17 +5166,17 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.10" +version = "0.19.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ - "indexmap", + "indexmap 2.0.0", "toml_datetime", "winnow", ] @@ -5167,6 +5190,56 @@ dependencies = [ "log", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8bd22a874a2d0b70452d5597b12c537331d49060824a95f49f108994f94aa4c" +dependencies = [ + "async-compression 0.3.15", + "bitflags 2.3.3", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -5180,16 +5253,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", - "pin-project-lite 0.2.9", + "log", + "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", @@ -5230,6 +5304,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.17" @@ -5240,12 +5324,16 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", + "time 0.3.22", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -5298,6 +5386,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -5306,9 +5403,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" [[package]] name = "unicode-normalization" @@ -5387,39 +5484,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4735c95b4cca1447b448e2e2e87e98d7e7498f4da27e355cf7af02204521001d" -dependencies = [ - "erased-serde", - "serde", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859cb4f0ce7da6a118b559ba74b0e63bf569bea867c20ba457a6b1c886a04e97" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" [[package]] name = "vcpkg" @@ -5646,7 +5713,7 @@ dependencies = [ "base64urlsafedata", "js-sys", "serde", - "serde-wasm-bindgen", + "serde-wasm-bindgen 0.4.5", "serde_json", "url", "wasm-bindgen", @@ -5726,22 +5793,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.48.1", ] [[package]] @@ -5759,7 +5811,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -5779,9 +5831,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", @@ -5964,7 +6016,7 @@ dependencies = [ "futures", "gloo", "implicit-clone", - "indexmap", + "indexmap 1.9.3", "js-sys", "prokio", "rustversion", @@ -6002,7 +6054,7 @@ checksum = "426ee0486d2572a6c5e39fbdbc48b58d59bb555f3326f54631025266cf04146e" dependencies = [ "gloo", "js-sys", - "route-recognizer 0.3.1", + "route-recognizer", "serde", "serde_urlencoded", "tracing", @@ -6043,6 +6095,36 @@ dependencies = [ "syn 2.0.23", ] +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "zxcvbn" version = "2.2.2" diff --git a/Cargo.toml b/Cargo.toml index b6d27550d..0fe1fbb78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ repository = "https://github.com/kanidm/kanidm/" argon2 = { version = "0.5.0", features = ["alloc"] } async-recursion = "1.0.4" async-trait = "^0.1.68" +axum = {version = "0.6.18", features = ["json", "http2", "macros", "tracing", "headers", "original-uri", "query", "form", "http2"]} +axum-csp = { version = "0.0.5" } base32 = "^0.4.0" base64 = "^0.21.0" base64urlsafedata = "0.1.3" @@ -67,20 +69,19 @@ futures = "^0.3.28" futures-concurrency = "^3.1.0" futures-util = { version = "^0.3.21", features = ["sink"] } gloo = "^0.8.1" -gloo-net = "0.3.0" hashbrown = { version = "0.14.0", features = ["serde", "inline-more", "ahash"] } hex = "^0.4.3" http-types = "^2.12.0" +hyper = { version = "0.14.27", features = ["full"] } +hyper-tls = "0.5.0" idlset = "^0.2.4" # idlset = { path = "../idlset" } js-sys = "^0.3.63" kanidmd_core = { path = "./server/core" } -kanidmd_idm = { path = "./server/idm" } kanidmd_lib = { path = "./server/lib" } kanidmd_lib_macros = { path = "./server/lib-macros" } kanidm_lib_crypto = { path = "./libs/crypto" } kanidm_lib_file_permissions = { path = "./libs/file_permissions" } -kanidmd_testkit = { path = "./server/testkit" } kanidm_client = { path = "./libs/client", version = "1.1.0-alpha.11" } kanidm_proto = { path = "./proto", version = "1.1.0-alpha.11" } kanidm_unix_int = { path = "./unix_integration" } @@ -113,7 +114,7 @@ qrcode = "^0.12.0" quote = "1" rand = "^0.8.5" regex = "1.8.4" -reqwest = { version = "0.11.18", default-features = false, features=["cookies", "json", "gzip", "native-tls"] } +reqwest = { version = "0.11.18", default-features = false, features=["cookies", "json", "gzip", "native-tls", "native-tls-alpn"] } rpassword = "^7.2.0" rusqlite = "^0.28.0" @@ -133,10 +134,6 @@ smolset = "^1.3.1" sshkeys = "^0.3.1" syn = { version = "2.0.23", features = ["full"] } testkit-macros = { path = "./server/testkit-macros" } -tide = "^0.16.0" -# Including brotli *very* slow, so don't do that. Including the "default" feature pulls a mime-type list from the internet on build, which isn't used. -tide-compress = { version="0.10.6", default-features = false, features = [ "gzip", "regex-check" ] } -tide-openssl = "^0.1.1" time = { version = "^0.3.21", features = ["formatting", "local-offset"] } tikv-jemallocator = "0.5" @@ -182,12 +179,5 @@ yew-router = "^0.17.0" zxcvbn = "^2.2.2" nonempty = "0.8.1" - -# enshrinken the WASMs -[profile.release.package.kanidmd_web_ui] -# optimization over all codebase ( better optimization, slower build ) -codegen-units = 1 -# optimization for size ( more aggressive ) -opt-level = 'z' # link time optimization using using whole-program analysis # lto = true diff --git a/Makefile b/Makefile index 27abc0fe0..8227de892 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ CONTAINER_BUILD_ARGS ?= MARKDOWN_FORMAT_ARGS ?= --options-line-width=100 CONTAINER_TOOL ?= docker BUILDKIT_PROGRESS ?= plain - +TESTS ?= BOOK_VERSION ?= master .DEFAULT: help @@ -15,6 +15,11 @@ BOOK_VERSION ?= master help: @grep -E -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' +.PHONY: run +run: ## Run the test/dev server +run: + cd server/daemon && ./run_insecure_dev_server.sh + .PHONY: buildx/kanidmd buildx/kanidmd: ## Build multiarch kanidm server images and push to docker hub buildx/kanidmd: @@ -259,3 +264,27 @@ webui: ## Build the WASM web frontend .PHONY: webui/test webui/test: ## Run wasm-pack test cd server/web_ui && wasm-pack test --headless --chrome + +.PHONY: rust/coverage +coverage/test: ## Run coverage tests +coverage/test: + LLVM_PROFILE_FILE="$(PWD)/target/profile/coverage-%p-%m.profraw" RUSTFLAGS="-C instrument-coverage" cargo test $(TESTS) + +.PHONY: coverage/grcov +coverage/grcov: ## Run grcov +coverage/grcov: + rm -rf ./target/coverage/html + grcov . --binary-path ./target/debug/deps/ \ + -s . \ + -t html \ + --branch \ + --ignore-not-existing \ + --ignore '../*' \ + --ignore "/*" \ + --ignore "target/*" \ + -o target/coverage/html + +.PHONY: coverage +coverage: ## Run all the coverage tests +coverage: coverage/test coverage/grcov + echo "Coverage report is in ./target/coverage/html/index.html" diff --git a/book/src/DEVELOPER_README.md b/book/src/DEVELOPER_README.md index b9da9e386..3d87ca917 100644 --- a/book/src/DEVELOPER_README.md +++ b/book/src/DEVELOPER_README.md @@ -1,6 +1,6 @@ -## Getting Started (for Developers) +# Getting Started (for Developers) -### Setup the Server +## Setup the Server It's important before you start trying to write code and contribute that you understand what Kanidm does and its goals. @@ -8,20 +8,21 @@ does and its goals. An important first step is to [install the server](installing_the_server.md) so if you have not done that yet, go and try that now! 😄 -### Setting up your Machine +## Setting up your Machine Each operating system has different steps required to configure and build Kanidm. -#### MacOS +### MacOS A prerequisite is [Apple Xcode](https://apps.apple.com/au/app/xcode/id497799835?mt=12) for access to git and compiler tools. You should install this first. You will need [rustup](https://rustup.rs/) to install a Rust toolchain. -To build the Web UI you'll need [wasm-pack](https://rustwasm.github.io/wasm-pack/) (`cargo install wasm-pack`). +To build the Web UI you'll need [wasm-pack](https://rustwasm.github.io/wasm-pack/) +(`cargo install wasm-pack`). -#### SUSE / OpenSUSE +### SUSE / OpenSUSE You will need to install rustup and our build dependencies with: @@ -31,7 +32,7 @@ zypper in rustup git libudev-devel sqlite3-devel libopenssl-3-devel You can then use rustup to complete the setup of the toolchain. -#### Fedora +### Fedora You will need [rustup](https://rustup.rs/) to install a Rust toolchain. @@ -47,7 +48,7 @@ Building the Web UI requires additional packages: perl-FindBin perl-File-Compare ``` -#### Ubuntu +### Ubuntu You need [rustup](https://rustup.rs/) to install a Rust toolchain. @@ -59,7 +60,7 @@ sudo apt-get install libsqlite3-dev libudev-dev libssl-dev pkg-config libpam0g-d Tested with Ubuntu 20.04 and 22.04. -#### Windows +### Windows @@ -94,7 +95,7 @@ vcpkg install openssl:x64-windows-static-md There's a powershell script in the root directory of the repository which, in concert with `openssl` will generate a config file and certs for testing. -### Getting the Source Code +## Getting the Source Code ### Get Involved @@ -157,7 +158,7 @@ git pull git branch -D ``` -#### Rebasing +### Rebasing If you are asked to rebase your change, follow these steps: @@ -196,25 +197,25 @@ cd book mdbook serve ``` -### Designs +## Designs See the "Design Documents" section of this book. -### Rust Documentation +## Rust Documentation A list of links to the library documentation is at [kanidm.com/documentation](https://kanidm.com/documentation/). -### Advanced +## Advanced -#### Minimum Supported Rust Version +### Minimum Supported Rust Version The MSRV is specified in the package `Cargo.toml` files. We tend to be quite proactive in updating this to recent rust versions so we are open to increasing this value if required! -#### Build Profiles +### Build Profiles Build profiles allow us to change the operation of Kanidm during it's compilation for development or release on various platforms. By default the "developer" profile is used that assumes the correct @@ -230,7 +231,7 @@ For example, this will set the CPU flags to "none" and the location for the Web KANIDM_BUILD_PROFILE=release_suse_generic cargo build --release --bin kanidmd ``` -#### Building the Web UI +### Building the Web UI **NOTE:** There is a pre-packaged version of the Web UI at `/server/web_ui/pkg/`, which can be used directly. This means you don't need to build the Web UI yourself. @@ -253,7 +254,7 @@ To build for release, run `build_wasm_release.sh`. The "developer" profile for kanidmd will automatically use the pkg output in this folder. -#### Development Server for Interactive Testing +### Development Server for Interactive Testing Especially if you wish to develop the WebUI then the ability to run the server from the source tree is critical. @@ -294,7 +295,7 @@ cargo run --bin kanidm -- self whoami -H https://localhost:8443 -D admin -C /tmp You may find it easier to modify `~/.config/kanidm` per the [book client tools section](client_tools.md) for extended administration locally. -#### Raw actions +### Raw actions @@ -326,7 +327,7 @@ kanidm raw search -H https://localhost:8443 -C ../insecure/ca.pem -D admin '{"eq kanidm raw delete -H https://localhost:8443 -C ../insecure/ca.pem -D idm_admin '{"eq": ["name", "test_account_delete_me"]}' ``` -#### Build a Kanidm Container +### Build a Kanidm Container Build a container with the current branch using: @@ -348,7 +349,7 @@ The following environment variables control the build: | `CONTAINER_TOOL` | Use an alternative container build tool. | `docker` | | `BOOK_VERSION` | Sets version used when building the documentation book. | `master` | -##### Container Build Examples +#### Container Build Examples Build a `kanidm` container using `podman`: @@ -362,7 +363,7 @@ Build a `kanidm` container and use a redis build cache: CONTAINER_BUILD_ARGS='--build-arg "SCCACHE_REDIS=redis://redis.dev.blackhats.net.au:6379"' make build/kanidmd ``` -##### Automatically Built Containers +#### Automatically Built Containers To speed up testing across platforms, we're leveraging GitHub actions to build containers for test use. diff --git a/book/src/developers/designs/content_security_policy.md b/book/src/developers/designs/content_security_policy.md new file mode 100644 index 000000000..cd165314e --- /dev/null +++ b/book/src/developers/designs/content_security_policy.md @@ -0,0 +1,7 @@ +# Content-Security-Policy Headers + +These are required on any path which browser-based clients access. + +- `/` +- `/ui/` +- `/pkg/` diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 26dda2291..44d003836 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -62,6 +62,8 @@ pub enum ClientError { JsonDecode(reqwest::Error, String), JsonEncode(SerdeJsonError), SystemError, + ConfigParseIssue(String), + CertParseIssue(String), } #[derive(Debug, Deserialize, Serialize)] @@ -136,7 +138,7 @@ impl KanidmClientBuilder { } } - fn parse_certificate(ca_path: &str) -> Result { + fn parse_certificate(ca_path: &str) -> Result { let mut buf = Vec::new(); // Is the CA secure? #[cfg(target_family = "windows")] @@ -145,7 +147,10 @@ impl KanidmClientBuilder { #[cfg(target_family = "unix")] { let path = Path::new(ca_path); - let ca_meta = read_file_metadata(&path)?; + let ca_meta = read_file_metadata(&path).map_err(|e| { + error!("{:?}", e); + ClientError::ConfigParseIssue(format!("{:?}", e)) + })?; trace!("uid:gid {}:{}", ca_meta.uid(), ca_meta.gid()); @@ -165,17 +170,20 @@ impl KanidmClientBuilder { // TODO #725: Handle these errors better, or at least provide diagnostics - this currently fails silently let mut f = File::open(ca_path).map_err(|e| { - error!(?e); + error!("{:?}", e); + ClientError::ConfigParseIssue(format!("{:?}", e)) })?; f.read_to_end(&mut buf).map_err(|e| { - error!(?e); + error!("{:?}", e); + ClientError::ConfigParseIssue(format!("{:?}", e)) })?; reqwest::Certificate::from_pem(&buf).map_err(|e| { - error!(?e); + error!("{:?}", e); + ClientError::CertParseIssue(format!("{:?}", e)) }) } - fn apply_config_options(self, kcc: KanidmClientConfig) -> Result { + fn apply_config_options(self, kcc: KanidmClientConfig) -> Result { let KanidmClientBuilder { address, verify_ca, @@ -213,7 +221,7 @@ impl KanidmClientBuilder { pub fn read_options_from_optional_config + std::fmt::Debug>( self, config_path: P, - ) -> Result { + ) -> Result { debug!("Attempting to load configuration from {:#?}", &config_path); // We have to check the .exists case manually, because there are some weird overlayfs @@ -257,11 +265,15 @@ impl KanidmClientBuilder { }; let mut contents = String::new(); - f.read_to_string(&mut contents) - .map_err(|e| error!("{:?}", e))?; + f.read_to_string(&mut contents).map_err(|e| { + error!("{:?}", e); + ClientError::ConfigParseIssue(format!("{:?}", e)) + })?; - let config: KanidmClientConfig = - toml::from_str(contents.as_str()).map_err(|e| error!("{:?}", e))?; + let config: KanidmClientConfig = toml::from_str(contents.as_str()).map_err(|e| { + error!("{:?}", e); + ClientError::ConfigParseIssue(format!("{:?}", e)) + })?; self.apply_config_options(config) } @@ -324,9 +336,12 @@ impl KanidmClientBuilder { } #[allow(clippy::result_unit_err)] - pub fn add_root_certificate_filepath(self, ca_path: &str) -> Result { + pub fn add_root_certificate_filepath(self, ca_path: &str) -> Result { //Okay we have a ca to add. Let's read it in and setup. - let ca = Self::parse_certificate(ca_path)?; + let ca = Self::parse_certificate(ca_path).map_err(|e| { + error!("{:?}", e); + ClientError::CertParseIssue(format!("{:?}", e)) + })?; Ok(KanidmClientBuilder { address: self.address, @@ -537,7 +552,7 @@ impl KanidmClient { request: R, ) -> Result { let dest = format!("{}{}", self.get_url(), dest); - + trace!("perform_auth_post_request connecting to {}", dest); let req_string = serde_json::to_string(&request).map_err(ClientError::JsonEncode)?; let response = self @@ -765,6 +780,7 @@ impl KanidmClient { .map_err(|e| ClientError::JsonDecode(e, opid)) } + #[instrument(level = "debug", skip(self))] async fn perform_get_request(&self, dest: &str) -> Result { let dest = format!("{}{}", self.get_url(), dest); let response = self.client.get(dest.as_str()); @@ -906,6 +922,7 @@ impl KanidmClient { .map_err(|e| ClientError::JsonDecode(e, opid)) } + #[instrument(level = "debug")] pub async fn auth_step_init(&self, ident: &str) -> Result, ClientError> { let auth_init = AuthRequest { step: AuthStep::Init2 { @@ -1068,11 +1085,13 @@ impl KanidmClient { } } + #[instrument(level = "debug")] pub async fn auth_simple_password( &self, ident: &str, password: &str, ) -> Result<(), ClientError> { + trace!("Init auth step"); let mechs = match self.auth_step_init(ident).await { Ok(s) => s, Err(e) => return Err(e), @@ -1508,6 +1527,7 @@ impl KanidmClient { } // == new credential update session code. + #[instrument(level = "debug", skip(self))] pub async fn idm_person_account_credential_update_intent( &self, id: &str, @@ -1776,6 +1796,7 @@ impl KanidmClient { } // ==== Oauth2 resource server configuration + #[instrument(level = "debug")] pub async fn idm_oauth2_rs_list(&self) -> Result, ClientError> { self.perform_get_request("/v1/oauth2").await } diff --git a/libs/file_permissions/src/lib.rs b/libs/file_permissions/src/lib.rs index efe6bab0b..9686dec7f 100644 --- a/libs/file_permissions/src/lib.rs +++ b/libs/file_permissions/src/lib.rs @@ -35,6 +35,20 @@ pub fn readonly(meta: &Metadata) -> bool { ) } +#[cfg(target_family = "unix")] +#[test] +fn test_readonly() { + // check if the file Cargo.toml exists + use std::path::Path; + if Path::new("Cargo.toml").exists() == false { + panic!("Can't find Cargo.toml"); + } + + let meta = std::fs::metadata("Cargo.toml").expect("Can't find Cargo.toml"); + println!("meta={:?} -> readonly={:?}", meta, readonly(&meta)); + assert!(readonly(&meta) == false); +} + #[cfg(not(target_family = "unix"))] /// Check a given file's metadata is read-only for the current user (true = read-only) Stub function if you're building for windows! pub fn readonly(meta: &Metadata) -> bool { diff --git a/libs/sketching/Cargo.toml b/libs/sketching/Cargo.toml index 07b5b4973..b3e213a0c 100644 --- a/libs/sketching/Cargo.toml +++ b/libs/sketching/Cargo.toml @@ -14,7 +14,6 @@ repository = { workspace = true } [dependencies] async-trait = { workspace = true } num_enum = { workspace = true } -tide = { workspace = true } tracing = { workspace = true, features = ["attributes"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing-forest = { workspace = true, features = ["uuid", "smallvec", "tokio", "env-filter"] } diff --git a/libs/sketching/src/lib.rs b/libs/sketching/src/lib.rs index a31319944..02e52651d 100644 --- a/libs/sketching/src/lib.rs +++ b/libs/sketching/src/lib.rs @@ -6,7 +6,6 @@ use tracing_forest::util::*; use tracing_forest::Tag; pub mod macros; -pub mod middleware; pub use {tracing, tracing_forest, tracing_subscriber}; diff --git a/libs/sketching/src/middleware.rs b/libs/sketching/src/middleware.rs deleted file mode 100644 index 171feb5b9..000000000 --- a/libs/sketching/src/middleware.rs +++ /dev/null @@ -1,107 +0,0 @@ -use tide::{self, Middleware, Next, Request}; -use tracing::{self, instrument}; - -use crate::{request_error, request_info, request_warn, security_info, *}; - -pub struct TreeMiddleware { - trust_x_forward_for: bool, -} - -impl TreeMiddleware { - pub fn new(trust_x_forward_for: bool) -> Self { - TreeMiddleware { - trust_x_forward_for, - } - } - - #[instrument(name = "tide-request", skip(self, req, next))] - async fn log<'a, State: Clone + Send + Sync + 'static>( - &'a self, - mut req: Request, - next: Next<'a, State>, - ) -> tide::Result { - struct TreeMiddlewareFinished; - - if req.ext::().is_some() { - return Ok(next.run(req).await); - } - req.set_ext(TreeMiddlewareFinished); - - let remote_address = if self.trust_x_forward_for { - req.remote() - } else { - req.peer_addr() - } - .unwrap_or("-") - .to_string(); - let host = req.host().unwrap_or("-").to_string(); - let method = req.method(); - let path = req.url().path().to_string(); - - let remote_address = remote_address.as_str(); - let host = host.as_str(); - let method = method.as_ref(); - let path = path.as_str(); - - security_info!( - remote_addr = remote_address, - http.host = host, - http.method = method, - path, - "Request received" - ); - - let response = next.run(req).await; - let status = response.status(); - - if status.is_server_error() { - if let Some(error) = response.error() { - request_error!( - message = display(error), - error_type = error.type_name().unwrap_or("?"), - status = format_args!("{} - {}", status as u16, status.canonical_reason()), - "Internal error -> Response sent" - ); - } else { - request_error!( - status = format_args!("{} - {}", status as u16, status.canonical_reason()), - "Internal error -> Response sent" - ); - } - } else if status.is_client_error() { - if let Some(error) = response.error() { - request_warn!( - message = display(error), - error_type = error.type_name().unwrap_or("?"), - status = format_args!("{} - {}", status as u16, status.canonical_reason()), - "Client error --> Response sent" - ); - } else if status == 404 { - // because not-found isn't really an error, is it? - request_info!( - status = format_args!("{} - {}", status as u16, status.canonical_reason()), - "--> Response sent" - ); - } else { - request_warn!( - status = format_args!("{} - {}", status as u16, status.canonical_reason()), - "Client error --> Response sent" - ); - } - } else { - request_info!( - status = format_args!("{} - {}", status as u16, status.canonical_reason()), - "--> Response sent" - ); - } - - Ok(response) - } -} - -#[async_trait::async_trait] -impl Middleware for TreeMiddleware { - async fn handle(&self, req: Request, next: Next<'_, State>) -> tide::Result { - self.log(req, next).await - } -} diff --git a/scripts/setup_dev_environment.sh b/scripts/setup_dev_environment.sh index b0adfce6a..3ed3c2206 100755 --- a/scripts/setup_dev_environment.sh +++ b/scripts/setup_dev_environment.sh @@ -96,7 +96,7 @@ echo "Adding ${TEST_USER_NAME} to ${TEST_GROUP}" ${KANIDM} group add-members "${TEST_GROUP}" "${TEST_USER_NAME}" -D idm_admin echo "Enable experimental UI for admin idm_admin ${TEST_USER_NAME}" -${KANIDM} group add-members idm_ui_enable_experimental_features admin idm_admin "${TEST_USER_NAME}" +${KANIDM} group add-members idm_ui_enable_experimental_features admin idm_admin "${TEST_USER_NAME}" -D idm_admin # create oauth2 rp echo "Creating the OAuth2 RP" @@ -108,6 +108,8 @@ echo "Creating the OAuth2 RP Supplemental Scope Map" ${KANIDM} system oauth2 update-sup-scope-map "${OAUTH2_RP_ID}" "${TEST_GROUP}" admin -D admin echo "Creating the OAuth2 RP Secondary Supplemental Crab-baite Scope Map.... wait, no that's not a thing." +echo "Checking the OAuth2 RP Exists" +${KANIDM} system oauth2 list -D admin | rg -A10 "${OAUTH2_RP_ID}" # config auth2 echo "Pulling secret for the OAuth2 RP" diff --git a/scripts/test_coverage.sh b/scripts/test_coverage.sh new file mode 100755 index 000000000..1898ae018 --- /dev/null +++ b/scripts/test_coverage.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +set -e + +if [ "$(rustup default | grep -cE '^nightly' )" -eq 0 ]; then + echo "You need to switch to rust nightly!" + exit 1 +fi + +# if [ "$(which rustfilt | wc -l )" -eq 0 ]; then +# echo "You need to have rustfilt on the path" +# echo "cargo install rustfilt" +# exit 1 +# fi +if [ "$(which llvm-cov | wc -l )" -eq 0 ]; then + echo "You need to have llvm-cov on the path" + exit 1 +fi +export CARGO_INCREMENTAL=0 + + +export LLVM_PROFILE_FILE +echo "Profile files going into ${LLVM_PROFILE_FILE}" + +echo "Running tests" +#shellcheck disable=SC2068 + +LLVM_PROFILE_FILE="$(pwd)/target/profile/coverage-%p-%m.profraw" RUSTFLAGS="-C instrument-coverage" cargo test + +grcov . --binary-path ./target/debug/deps/ \ + -s . \ + -t html \ + --branch \ + --ignore-not-existing \ + --ignore '../*' \ + --ignore "/*" \ + -o target/coverage/html + + +# PROFDATA="./target/profile/kanidm.profdata" + +# llvm-profdata merge ./target/profile/*.profraw -o "${PROFDATA}" + +# llvm-cov report --ignore-filename-regex="\.cargo" \ +# --enable-name-compression \ +# $( \ +# for file in \ +# $( \ +# RUSTFLAGS="-C instrument-coverage" \ +# cargo test --tests --no-run --message-format=json \ +# | jq -r "select(.profile.test == true) | .filenames[]" \ +# | grep -v dSYM - \ +# ); \ +# do \ +# printf "%s %s " -object $file; \ +# done \ +# ) \ +# --instr-profile="${PROFDATA}" --summary-only + +# llvm-cov show -Xdemangler=rustfilt target/debug/kanidmd \ +# -instr-profile="${PROFDATA}" \ +# -show-line-counts-or-regions \ +# -show-instantiations \ +# -name-regex="kani.*" \ No newline at end of file diff --git a/server/core/Cargo.toml b/server/core/Cargo.toml index ae7493625..775b435c8 100644 --- a/server/core/Cargo.toml +++ b/server/core/Cargo.toml @@ -14,11 +14,20 @@ repository = { workspace = true } [dependencies] async-trait = { workspace = true } +axum = { workspace=true } +axum-auth = "0.4.0" +axum-csp = { workspace = true } +axum-macros = "0.3.7" +axum-server = { version = "0.5.1", features = ["tls-openssl"] } +axum-sessions = "0.5.0" chrono = { workspace = true } -cron = { workspace = true } compact_jwt = { workspace = true } +cron = { workspace = true } futures-util = { workspace = true } +http = "0.2.9" http-types = { workspace = true } +hyper = { workspace = true } +hyper-tls = { workspace = true } kanidm_proto = { workspace = true } kanidmd_lib = { workspace = true } ldap3_proto = { workspace = true } @@ -26,18 +35,19 @@ libc = { workspace = true } openssl = { workspace = true } rand = { workspace = true } regex = { workspace = true } +reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sketching = { workspace = true } -tide = { workspace = true } time = { workspace = true, features = ["serde", "std","local-offset"] } -tide-compress = { workspace = true } -tide-openssl = { workspace = true } tokio = { workspace = true, features = ["net", "sync", "io-util", "macros"] } tokio-openssl = { workspace = true } tokio-util = { workspace = true, features = ["codec"] } toml = {workspace = true} +tower = { version = "0.4.13", features = ["tokio-stream", "tracing"] } +tower-http = { version = "0.4.1", features = ["tokio", "tracing", "uuid", "compression-gzip", "compression-zstd", "trace", "fs"] } tracing = { workspace = true, features = ["attributes"] } +tracing-subscriber = { workspace = true, features = ["time", "json"] } urlencoding = { workspace = true } uuid = { workspace = true, features = ["serde", "v4" ] } diff --git a/server/core/src/actors/v1_read.rs b/server/core/src/actors/v1_read.rs index b22bd782d..8f5cac5bc 100644 --- a/server/core/src/actors/v1_read.rs +++ b/server/core/src/actors/v1_read.rs @@ -212,6 +212,7 @@ impl QueryServerReadV1 { time::OffsetDateTime::now_utc() } }; + #[allow(clippy::unwrap_used)] let timestamp = now.format(&Rfc3339).unwrap(); let dest_file = format!("{}/backup-{}.json", outpath, timestamp); diff --git a/server/core/src/actors/v1_write.rs b/server/core/src/actors/v1_write.rs index ed7c71e0c..bad1a4753 100644 --- a/server/core/src/actors/v1_write.rs +++ b/server/core/src/actors/v1_write.rs @@ -290,16 +290,12 @@ impl QueryServerWriteV1 { e })?; - let mdf = ModifyEvent::from_internal_parts( - ident, - &modlist, - &filter, - &mut idms_prox_write.qs_write, - ) - .map_err(|e| { - admin_error!(err = ?e, "Failed to begin modify"); - e - })?; + let mdf = + ModifyEvent::from_internal_parts(ident, &modlist, &filter, &idms_prox_write.qs_write) + .map_err(|e| { + admin_error!(err = ?e, "Failed to begin modify"); + e + })?; trace!(?mdf, "Begin modify event"); @@ -644,7 +640,7 @@ impl QueryServerWriteV1 { #[instrument( level = "info", skip_all, - fields(uuid = ?eventid) + fields(uuid = ?eventid), )] pub async fn handle_idmcredentialupdateintent( &self, diff --git a/server/core/src/config.rs b/server/core/src/config.rs index 783ba18ee..cde97ef32 100644 --- a/server/core/src/config.rs +++ b/server/core/src/config.rs @@ -15,13 +15,13 @@ use kanidm_proto::messages::ConsoleOutputMode; use serde::{Deserialize, Serialize}; use sketching::tracing_subscriber::EnvFilter; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct IntegrationTestConfig { pub admin_user: String, pub admin_password: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct OnlineBackup { pub path: String, #[serde(default = "default_online_backup_schedule")] @@ -38,7 +38,7 @@ fn default_online_backup_versions() -> usize { 7 } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct TlsConfiguration { pub chain: String, pub key: String, @@ -64,16 +64,22 @@ pub struct ServerConfig { } impl ServerConfig { - pub fn new>(config_path: P) -> Result { + pub fn new>(config_path: P) -> Result { let mut f = File::open(config_path).map_err(|e| { eprintln!("Unable to open config file [{:?}] 🥺", e); + e })?; let mut contents = String::new(); - f.read_to_string(&mut contents) - .map_err(|e| eprintln!("unable to read contents {:?}", e))?; + f.read_to_string(&mut contents).map_err(|e| { + eprintln!("unable to read contents {:?}", e); + e + })?; - toml::from_str(contents.as_str()).map_err(|e| eprintln!("unable to parse config {:?}", e)) + toml::from_str(contents.as_str()).map_err(|e| { + eprintln!("unable to parse config {:?}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + }) } } @@ -142,9 +148,9 @@ impl ToString for LogLevel { } } -impl Into for LogLevel { - fn into(self) -> EnvFilter { - match self { +impl From for EnvFilter { + fn from(value: LogLevel) -> Self { + match value { LogLevel::Info => EnvFilter::new("info"), LogLevel::Debug => EnvFilter::new("debug"), LogLevel::Trace => EnvFilter::new("trace"), @@ -152,7 +158,7 @@ impl Into for LogLevel { } } -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct Configuration { pub address: String, pub ldapaddress: Option, diff --git a/server/core/src/https/generic.rs b/server/core/src/https/generic.rs new file mode 100644 index 000000000..4a5ec6f67 --- /dev/null +++ b/server/core/src/https/generic.rs @@ -0,0 +1,33 @@ +use axum::extract::State; +use axum::response::{IntoResponse, Response}; +use axum::Extension; +use http::header::CONTENT_TYPE; +use kanidmd_lib::status::StatusRequestEvent; + +use super::middleware::KOpId; +use super::ServerState; + +/// Status endpoint used for healthchecks +pub async fn status( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { + let r = state + .status_ref + .handle_request(StatusRequestEvent { + eventid: kopid.eventid, + }) + .await; + Response::new(format!("{}", r)) +} + +pub async fn robots_txt() -> impl IntoResponse { + ( + [(CONTENT_TYPE, "text/plain;charset=utf-8")], + axum::response::Html( + r#"User-agent: * + Disallow: / +"#, + ), + ) +} diff --git a/server/core/src/https/javascript.rs b/server/core/src/https/javascript.rs new file mode 100644 index 000000000..121cafcbc --- /dev/null +++ b/server/core/src/https/javascript.rs @@ -0,0 +1,57 @@ +use std::path::PathBuf; + +/// Generates the integrity hash for a file based on a filename +pub fn generate_integrity_hash(filename: String) -> Result { + let wasm_filepath = PathBuf::from(filename); + match wasm_filepath.exists() { + false => Err(format!( + "Can't find {:?} to generate file hash", + &wasm_filepath + )), + true => { + let filecontents = match std::fs::read(&wasm_filepath) { + Ok(value) => value, + Err(error) => { + return Err(format!( + "Failed to read {:?}, skipping: {:?}", + wasm_filepath, error + )); + } + }; + let shasum = openssl::hash::hash(openssl::hash::MessageDigest::sha384(), &filecontents) + .map_err(|_| { + format!( + "Failed to generate SHA384 hash for WASM at {:?}", + wasm_filepath + ) + })?; + Ok(openssl::base64::encode_block(&shasum)) + } + } +} + +#[derive(Clone)] +pub struct JavaScriptFile { + // Relative to the pkg/ dir + pub filepath: &'static str, + // SHA384 hash of the file + pub hash: String, + // if it's a module add the "type" + pub filetype: Option, +} + +impl JavaScriptFile { + /// returns a `"#, + self.filepath, &self.hash, &typeattr, + ) + } +} diff --git a/server/core/src/https/manifest.rs b/server/core/src/https/manifest.rs index bc3d8557a..8dca2a20c 100644 --- a/server/core/src/https/manifest.rs +++ b/server/core/src/https/manifest.rs @@ -1,23 +1,29 @@ +//! Builds a Progressive Web App Manifest page. + +use axum::extract::State; +use axum::response::{IntoResponse, Response}; +use axum::Extension; +use http::HeaderValue; use serde::{Deserialize, Serialize}; -///! Builds a Progressive Web App Manifest page. +use super::middleware::KOpId; // Thanks to the webmanifest crate for a lot of this code -use crate::https::{AppState, RequestExtensions}; +use super::ServerState; /// The MIME type for `.webmanifest` files. const MIME_TYPE_MANIFEST: &str = "application/manifest+json;charset=utf-8"; /// Create a new manifest builder. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Manifest<'s> { - name: &'s str, - short_name: &'s str, - start_url: &'s str, +pub struct Manifest { + name: String, + short_name: String, + start_url: String, #[serde(rename = "display")] display_mode: DisplayMode, - background_color: &'s str, + background_color: String, #[serde(skip_serializing_if = "Option::is_none")] - description: Option<&'s str>, + description: Option, #[serde(rename = "dir")] direction: Direction, // direction: Option, @@ -25,11 +31,11 @@ pub struct Manifest<'s> { orientation: Option, // orientation: Option, #[serde(skip_serializing_if = "Option::is_none")] - lang: Option<&'s str>, + lang: Option, #[serde(skip_serializing_if = "Option::is_none")] - scope: Option<&'s str>, + scope: Option, // #[serde(skip_serializing_if = "Option::is_none")] - theme_color: &'s str, + theme_color: String, #[serde(skip_serializing_if = "Option::is_none")] prefer_related_applications: Option, // #[serde(borrow)] @@ -94,12 +100,7 @@ enum DisplayMode { Browser, } -/// Generates a manifest.json file for progressive web app usage -pub async fn manifest(req: tide::Request) -> tide::Result { - let mut res = tide::Response::new(200); - let (eventid, _) = req.new_eventid(); - let domain_display_name = req.state().qe_r_ref.get_domain_display_name(eventid).await; - +pub fn manifest_data(host_req: Option<&str>, domain_display_name: String) -> Manifest { let icons = vec![ ManifestIcon { sizes: String::from("512x512"), @@ -127,32 +128,45 @@ pub async fn manifest(req: tide::Request) -> tide::Result { }, ]; - let start_url = match req.host() { + let start_url = match host_req { Some(value) => format!("https://{}/", value), None => String::from("/"), }; - let manifest_struct = Manifest { - short_name: "Kanidm", - name: domain_display_name.as_str(), - start_url: start_url.as_str(), + Manifest { + short_name: "Kanidm".to_string(), + name: domain_display_name, + start_url, display_mode: DisplayMode::MinimalUi, description: None, orientation: None, - lang: Some("en"), - theme_color: "white", - background_color: "white", + lang: Some("en".to_string()), + theme_color: "white".to_string(), + background_color: "white".to_string(), direction: Direction::Auto, scope: None, prefer_related_applications: None, icons, related_applications: None, - }; - - let manifest_string = serde_json::to_string_pretty(&manifest_struct)?; - - res.set_content_type(MIME_TYPE_MANIFEST); - res.set_body(manifest_string); - - Ok(res) + } +} + +/// Generates a manifest.json file for progressive web app usage +pub(crate) async fn manifest( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { + let domain_display_name = state.qe_r_ref.get_domain_display_name(kopid.eventid).await; + // TODO: fix the None here to make it the request host + let manifest_string = + match serde_json::to_string_pretty(&manifest_data(None, domain_display_name)) { + Ok(val) => val, + Err(_) => String::from(""), + }; + let mut res = Response::new(manifest_string); + + res.headers_mut() + .insert("Content-Type", HeaderValue::from_static(MIME_TYPE_MANIFEST)); + + res } diff --git a/server/core/src/https/middleware.rs b/server/core/src/https/middleware.rs deleted file mode 100644 index eadf4281c..000000000 --- a/server/core/src/https/middleware.rs +++ /dev/null @@ -1,229 +0,0 @@ -use regex::Regex; - -///! Custom tide middleware for Kanidm -use crate::https::JavaScriptFile; - -/// This is for the tide_compression middleware so that we only compress certain content types. -/// -/// ``` -/// use kanidmd_core::https::middleware::compression_content_type_checker; -/// let these_should_match = vec![ -/// "application/wasm", -/// "application/x-javascript", -/// "application/x-javascript; charset=utf-8", -/// "image/svg+xml", -/// "text/json", -/// "text/javascript", -/// ]; -/// for test_value in these_should_match { -/// eprintln!("checking {:?}", test_value); -/// assert!(compression_content_type_checker().is_match(test_value)); -/// } -/// assert!(compression_content_type_checker().is_match("application/wasm")); -/// let these_should_be_skipped = vec![ -/// "application/manifest+json", -/// "image/jpeg", -/// "image/wasm", -/// "text/html", -/// ]; -/// for test_value in these_should_be_skipped { -/// eprintln!("checking {:?}", test_value); -/// assert!(!compression_content_type_checker().is_match(test_value)); -/// } -/// ``` -pub fn compression_content_type_checker() -> Regex { - Regex::new(r"^(?:(image/svg\+xml)|(?:application|text)/(?:css|javascript|json|text|x-javascript|xml|wasm))(|; charset=utf-8)$") - .expect("regex matcher for tide_compress content-type check failed to compile") -} - -#[derive(Default)] -pub struct CacheableMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for CacheableMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - response.insert_header("Cache-Control", "max-age=300,must-revalidate,private"); - Ok(response) - } -} - -#[derive(Default)] -pub struct NoCacheMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for NoCacheMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - response.insert_header("Cache-Control", "no-store, max-age=0"); - response.insert_header("Pragma", "no-cache"); - Ok(response) - } -} - -#[derive(Default)] -/// Sets Cache-Control headers on static content endpoints -pub struct StaticContentMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for StaticContentMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - response.insert_header("Cache-Control", "max-age=3600,private"); - Ok(response) - } -} - -#[derive(Default)] -/// Adds the following headers to responses -/// - x-frame-options -/// - x-content-type-options -/// - cross-origin-resource-policy -/// - cross-origin-embedder-policy -/// - cross-origin-opener-policy -pub struct StrictResponseMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for StrictResponseMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - response.insert_header("cross-origin-embedder-policy", "require-corp"); - response.insert_header("cross-origin-opener-policy", "same-origin"); - response.insert_header("cross-origin-resource-policy", "same-origin"); - response.insert_header("x-content-type-options", "nosniff"); - Ok(response) - } -} -#[derive(Default)] -struct StrictRequestMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for StrictRequestMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let proceed = request - .header("sec-fetch-site") - .map(|hv| { - matches!(hv.as_str(), "same-origin" | "same-site" | "none") - || (request.header("sec-fetch-mode").map(|v| v.as_str()) == Some("navigate") - && request.method() == tide::http::Method::Get - && request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("object") - && request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("embed")) - }) - .unwrap_or(true); - - if proceed { - Ok(next.run(request).await) - } else { - Err(tide::Error::from_str( - tide::StatusCode::MethodNotAllowed, - "StrictRequestViolation", - )) - } - } -} - -#[derive(Default)] -/// This tide MiddleWare adds headers like Content-Security-Policy -/// and similar families. If it keeps adding more things then -/// probably rename the middleware :) -pub struct UIContentSecurityPolicyResponseMiddleware { - // The sha384 hash of /pkg/wasmloader.js - pub hashes: Vec, -} -impl UIContentSecurityPolicyResponseMiddleware { - pub fn new(hashes: Vec) -> Self { - Self { hashes } - } -} - -#[async_trait::async_trait] -impl tide::Middleware - for UIContentSecurityPolicyResponseMiddleware -{ - // This updates the UI body with the integrity hash value for the wasmloader.js file, and adds content-security-policy headers. - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - - // a list of hashes of js files that we're sending to the user - let hashes: Vec = self - .hashes - .iter() - .map(|j| format!("'{}'", j.hash)) - .collect(); - - response.insert_header( - /* content-security-policy headers tell the browser what to trust - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy - - In this case we're only trusting the same server that the page is - loaded from, and adding a hash of wasmloader.js, which is the main script - we should be loading, and should be really secure about that! - - */ - "content-security-policy", - vec![ - "default-src 'self'", - // TODO: #912 have a dev/test mode where we can rebuild the hashes on page load, so when doing constant JS changes/rebuilds we don't have to restart the server every time. It'd be *terrible* to run in prod because of the constant disk thrashing, but nicer for devs. - // we need unsafe-eval because of WASM things - format!("script-src 'self' {} 'unsafe-eval'", hashes.join(" ")).as_str(), - "form-action https: 'self'", // to allow for OAuth posts - // we are not currently using workers so it can be blocked - "worker-src 'none'", - // TODO: Content-Security-Policy-Report-Only https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only - // "report-to 'none'", // unsupported by a lot of things still, but mozilla's saying report-uri is deprecated? - // Commented because when violated this attempts to post to "'none'" as a url - // "report-uri 'none'", - "base-uri 'self'", - // nobody wants to be in a frame - "frame-ancestors 'none'", - // allow inline images because bootstrap - "img-src 'self' data:", - ] - .join(";"), - ); - - Ok(response) - } -} - -const KANIDM_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[derive(Default)] -pub struct VersionHeaderMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for VersionHeaderMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - response.insert_header("X-KANIDM-VERSION", KANIDM_VERSION); - Ok(response) - } -} diff --git a/server/core/src/https/middleware/caching.rs b/server/core/src/https/middleware/caching.rs new file mode 100644 index 000000000..61df78ad1 --- /dev/null +++ b/server/core/src/https/middleware/caching.rs @@ -0,0 +1,20 @@ +use axum::{ + http::{self, Request}, + middleware::Next, + response::Response, +}; + +/// Adds `no-cache max-age=0` to the response headers. +pub async fn dont_cache_me(request: Request, next: Next) -> Response { + let mut response = next.run(request).await; + response.headers_mut().insert( + http::header::CACHE_CONTROL, + http::HeaderValue::from_static("no-store no-cache max-age=0"), + ); + response.headers_mut().insert( + http::header::PRAGMA, + http::HeaderValue::from_static("no-cache"), + ); + + response +} diff --git a/server/core/src/https/middleware/compression.rs b/server/core/src/https/middleware/compression.rs new file mode 100644 index 000000000..9528dca6c --- /dev/null +++ b/server/core/src/https/middleware/compression.rs @@ -0,0 +1,25 @@ +/* + Let's build a compression middleware! + + The threat of the TLS BREACH attack [1] was considered as part of adding + the CompressMiddleware configuration. + + The attack targets secrets compressed and encrypted in flight with the intent + to infer their content. + + This is not a concern for the paths covered by this configuration + ( /, /ui/, /pkg/ ), + as they're all static content with no secrets in transit - all that data should + come from Kanidm's REST API, which is on a different path and not covered by + the compression middleware. + + + [1] - https://resources.infosecinstitute.com/topic/the-breach-attack/ +*/ + +use tower_http::compression::CompressionLayer; +pub fn new() -> CompressionLayer { + CompressionLayer::new() + .no_br() + .quality(tower_http::CompressionLevel::Best) +} diff --git a/server/core/src/https/middleware/csp_headers.rs b/server/core/src/https/middleware/csp_headers.rs new file mode 100644 index 000000000..e575b4fbe --- /dev/null +++ b/server/core/src/https/middleware/csp_headers.rs @@ -0,0 +1,21 @@ +use axum::extract::State; +use axum::http::Request; +use axum::middleware::Next; +use axum::response::Response; + +use crate::https::ServerState; + +pub async fn cspheaders_layer( + State(state): State, + request: Request, + next: Next, +) -> Response { + // wait for the middleware to come back + let mut response = next.run(request).await; + + // add the header + let headers = response.headers_mut(); + headers.insert("Content-Security-Policy", state.csp_header); + + response +} diff --git a/server/core/src/https/middleware/mod.rs b/server/core/src/https/middleware/mod.rs new file mode 100644 index 000000000..04333afb8 --- /dev/null +++ b/server/core/src/https/middleware/mod.rs @@ -0,0 +1,89 @@ +use axum::{ + headers::{authorization::Bearer, Authorization}, + http::{self, Request}, + middleware::Next, + response::Response, + Extension, TypedHeader, +}; +use axum_sessions::SessionHandle; +use http::HeaderValue; +use uuid::Uuid; + +pub(crate) mod caching; +pub(crate) mod compression; +pub(crate) mod csp_headers; + +// the version middleware injects +const KANIDM_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Injects a header into the response with "X-KANIDM-VERSION" matching the version of the package. +pub async fn version_middleware(request: Request, next: Next) -> Response { + let mut response = next.run(request).await; + let headers = response.headers_mut(); + headers.insert("X-KANIDM-VERSION", HeaderValue::from_static(KANIDM_VERSION)); + + response +} + +#[derive(Clone, Debug)] +/// For holding onto the event ID and other handy request-based things +pub struct KOpId { + pub eventid: Uuid, + pub uat: Option, +} + +impl KOpId { + /// Return the event ID as a string + pub fn eventid_value(&self) -> String { + let res = self.eventid; + res.as_hyphenated().to_string() + } +} + +/// This runs at the start of the request, adding an extension with `KOpId` which has useful things inside it. +pub async fn kopid_start( + auth: Option>>, + mut request: Request, + next: Next, +) -> Response { + // generate the event ID + let eventid = sketching::tracing_forest::id(); + + // get the bearer token from the headers or the session + let uat = match auth { + Some(bearer) => Some(bearer.token().to_string()), + None => { + // no headers, let's try the cookies + match request.extensions().get::() { + Some(sess) => { + // we have a session! + sess.read().await.get::("bearer") + } + None => None, + } + } + }; + + // insert the extension so we can pull it out later + request.extensions_mut().insert(KOpId { eventid, uat }); + next.run(request).await +} + +/// This runs at the start of the request, adding an extension with the OperationID +pub async fn kopid_end( + Extension(kopid): Extension, + request: Request, + next: Next, +) -> Response { + // generate the event ID + // insert the extension so we can pull it out later + let mut response = next.run(request).await; + + #[allow(clippy::unwrap_used)] + response.headers_mut().insert( + "X-KANIDM-OPID", + HeaderValue::from_str(&kopid.eventid_value()).unwrap(), + ); + + response +} diff --git a/server/core/src/https/mod.rs b/server/core/src/https/mod.rs index c003c6693..3ff9c713b 100644 --- a/server/core/src/https/mod.rs +++ b/server/core/src/https/mod.rs @@ -1,408 +1,106 @@ +mod generic; +mod javascript; mod manifest; -pub mod middleware; +mod middleware; mod oauth2; -mod routemaps; +mod tests; +mod ui; mod v1; mod v1_scim; -use std::fs::canonicalize; -use std::net::{IpAddr, SocketAddr}; -use std::path::PathBuf; -use std::str::FromStr; - -use compact_jwt::{Jws, JwsSigner, JwsUnverified, JwsValidator}; -use kanidmd_lib::prelude::*; -use kanidmd_lib::status::StatusActor; -use serde::Serialize; -use tide::listener::{Listener, ToListener}; -use tide_compress::CompressMiddleware; -use tide_openssl::TlsListener; -use tracing::{error, info}; -use uuid::Uuid; - -use self::manifest::manifest; -use self::middleware::*; -use self::oauth2::*; -use self::routemaps::{RouteMap, RouteMaps}; -use self::v1::*; -use self::v1_scim::*; use crate::actors::v1_read::QueryServerReadV1; use crate::actors::v1_write::QueryServerWriteV1; -use crate::config::{ServerRole, TlsConfiguration}; +use crate::config::{Configuration, ServerRole, TlsConfiguration}; +use axum::extract::connect_info::{IntoMakeServiceWithConnectInfo, ResponseFuture}; +use axum::middleware::{from_fn, from_fn_with_state}; +use axum::response::{Redirect, Response}; +use axum::routing::*; +use axum::Router; +use axum_csp::{CspDirectiveType, CspValue}; +use axum_macros::FromRef; +use axum_sessions::extractors::WritableSession; +use axum_sessions::{async_session, SameSite, SessionLayer}; +use compact_jwt::{Jws, JwsSigner, JwsUnverified}; +use generic::*; +use http::{HeaderMap, HeaderValue}; +use hyper::server::accept::Accept; +use hyper::server::conn::{AddrStream, Http}; +use hyper::Body; +use javascript::*; +use kanidm_proto::v1::OperationError; +use kanidmd_lib::status::StatusActor; +use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod}; +use tokio_openssl::SslStream; + +use futures_util::future::poll_fn; +use serde::Serialize; +use tokio::net::TcpListener; + +use std::io::ErrorKind; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; +use std::{net::SocketAddr, str::FromStr}; +use tokio::sync::broadcast; +use tower_http::services::ServeDir; +use tower_http::trace::TraceLayer; +use uuid::Uuid; use crate::CoreAction; -use tokio::sync::broadcast; -#[derive(Clone)] -pub struct JavaScriptFile { - // Relative to the pkg/ dir - filepath: &'static str, - // SHA384 hash of the file - hash: String, - // if it's a module add the "type" - filetype: Option, -} +use self::v1::SessionId; -impl JavaScriptFile { - /// return the hash for use in CSP headers - pub fn as_csp_hash(self) -> String { - self.hash - } - - /// returns a `"#, - self.filepath, &self.hash, &typeattr, - ) - } -} - -#[test] -fn test_javscriptfile() { - // make sure it outputs what we think it does - use JavaScriptFile; - let jsf = JavaScriptFile { - filepath: "wasmloader.js", - hash: "sha384-1234567890".to_string(), - filetype: Some("module".to_string()), - }; - assert_eq!( - jsf.as_tag(), - r#""# - ); - let jsf = JavaScriptFile { - filepath: "wasmloader.js", - hash: "sha384-1234567890".to_string(), - filetype: None, - }; - assert_eq!( - jsf.as_tag(), - r#""# - ); -} - -#[derive(Clone)] -pub struct AppState { - pub status_ref: &'static StatusActor, - pub qe_w_ref: &'static QueryServerWriteV1, - pub qe_r_ref: &'static QueryServerReadV1, +#[derive(Clone, FromRef)] +pub struct ServerState { + pub status_ref: &'static kanidmd_lib::status::StatusActor, + pub qe_w_ref: &'static crate::actors::v1_write::QueryServerWriteV1, + pub qe_r_ref: &'static crate::actors::v1_read::QueryServerReadV1, // Store the token management parts. - pub jws_signer: std::sync::Arc, - pub jws_validator: std::sync::Arc, - /// The SHA384 hashes of javascript files we're going to serve to users + pub jws_signer: compact_jwt::JwsSigner, + pub jws_validator: compact_jwt::JwsValidator, + // The SHA384 hashes of javascript files we're going to serve to users pub js_files: Vec, - pub(crate) trust_x_forward_for: bool, + // pub(crate) trust_x_forward_for: bool, + pub csp_header: HeaderValue, } -pub trait RequestExtensions { - fn get_current_uat(&self) -> Option; - - fn get_auth_bearer(&self) -> Option; - - fn get_current_auth_session_id(&self) -> Option; - - fn get_url_param(&self, param: &str) -> Result; - - fn get_url_param_uuid(&self, param: &str) -> Result; - - fn new_eventid(&self) -> (Uuid, String); - - fn get_remote_addr(&self) -> Option; -} - -impl RequestExtensions for tide::Request { - fn get_auth_bearer(&self) -> Option { - // Contact the QS to get it to validate wtf is up. - // let kref = &self.state().bundy_handle; - // self.session().get::("uat") - self.header(tide::http::headers::AUTHORIZATION) - .and_then(|hv| { - // Get the first header value. - hv.get(0) - }) - .and_then(|h| { - // Turn it to a &str, and then check the prefix - h.as_str().strip_prefix("Bearer ") - }) - .map(str::to_string) +impl ServerState { + fn reinflate_uuid_from_bytes(&self, input: &str) -> Option { + match JwsUnverified::from_str(input) { + Ok(val) => val + .validate(&self.jws_validator) + .map(|jws: Jws| jws.into_inner().sessionid) + .ok(), + Err(_) => None, + } } - fn get_current_uat(&self) -> Option { - // Contact the QS to get it to validate wtf is up. - // let kref = &self.state().bundy_handle; - // self.session().get::("uat") - self.header(tide::http::headers::AUTHORIZATION) - .and_then(|hv| { - // Get the first header value. - hv.get(0) - }) - .and_then(|h| { - // Turn it to a &str, and then check the prefix - h.as_str().strip_prefix("Bearer ") - }) - .map(|s| s.to_string()) - .or_else(|| self.session().get::("bearer")) - } - - fn get_current_auth_session_id(&self) -> Option { + fn get_current_auth_session_id( + &self, + headers: &HeaderMap, + session: &WritableSession, + ) -> Option { // We see if there is a signed header copy first. - let kref = &self.state().jws_validator; - self.header("X-KANIDM-AUTH-SESSION-ID") + headers + .get("X-KANIDM-AUTH-SESSION-ID") .and_then(|hv| { // Get the first header value. - hv.get(0) - }) - .and_then(|h| { - // Take the token str and attempt to decrypt - // Attempt to re-inflate a uuid from bytes. - JwsUnverified::from_str(h.as_str()).ok() - }) - .and_then(|jwsu| { - jwsu.validate(kref) - .map(|jws: Jws| jws.into_inner().sessionid) - .ok() + hv.to_str().ok() }) + .and_then(|s| Some(self.reinflate_uuid_from_bytes(s)).unwrap_or(None)) // If not there, get from the cookie instead. - .or_else(|| self.session().get::("auth-session-id")) - } - - fn get_url_param(&self, param: &str) -> Result { - self.param(param) - .map_err(|e| { - error!(?e); - tide::Error::from_str(tide::StatusCode::ImATeapot, "teapot") - }) - .and_then(|data| { - urlencoding::decode(data) - .map(|s| s.into_owned()) - .map_err(|e| { - error!(?e); - tide::Error::from_str(tide::StatusCode::ImATeapot, "teapot") - }) - }) - } - - fn get_url_param_uuid(&self, param: &str) -> Result { - self.param(param) - .map_err(|e| { - error!(?e); - tide::Error::from_str(tide::StatusCode::ImATeapot, "teapot") - }) - .and_then(|s| { - Uuid::try_parse(s).map_err(|e| { - error!(?e); - tide::Error::from_str(tide::StatusCode::ImATeapot, "teapot") - }) - }) - } - - fn new_eventid(&self) -> (Uuid, String) { - let eventid = sketching::tracing_forest::id(); - let hv = eventid.as_hyphenated().to_string(); - (eventid, hv) - } - - /// Returns the remote address of the client, based on if you've got trust_x_forward_for set in config. - fn get_remote_addr(&self) -> Option { - if self.state().trust_x_forward_for { - // xff headers don't have a port, but if we're going direct you might get one - let res = self.remote().and_then(|ip| { - ip.parse::() - .ok() - .or_else(|| ip.parse::().map(|s_ad| s_ad.ip()).ok()) - }); - debug!("Trusting XFF, using remote src_ip={:?}", res); - res - } else { - let res = self - .peer_addr() - .map(|addr| addr.parse::().unwrap()) - .map(|s_ad: SocketAddr| s_ad.ip()); - debug!("Not trusting XFF, using peer_addr src_ip={:?}", res); - res - } + .or_else(|| session.get::("auth-session-id")) } } -pub fn to_tide_response( - v: Result, - hvalue: String, -) -> tide::Result { - match v { - Ok(iv) => { - let mut res = tide::Response::new(200); - tide::Body::from_json(&iv).map(|b| { - res.set_body(b); - res - }) - } - Err(e) => { - let mut res = match &e { - OperationError::NotAuthenticated | OperationError::SessionExpired => { - // https://datatracker.ietf.org/doc/html/rfc7235#section-4.1 - let mut res = tide::Response::new(tide::StatusCode::Unauthorized); - res.insert_header("WWW-Authenticate", "Bearer"); - res - } - OperationError::SystemProtectedObject | OperationError::AccessDenied => { - tide::Response::new(tide::StatusCode::Forbidden) - } - OperationError::NoMatchingEntries => { - tide::Response::new(tide::StatusCode::NotFound) - } - OperationError::PasswordQuality(_) - | OperationError::EmptyRequest - | OperationError::SchemaViolation(_) => { - tide::Response::new(tide::StatusCode::BadRequest) - } - _ => tide::Response::new(tide::StatusCode::InternalServerError), - }; - tide::Body::from_json(&e).map(|b| { - res.set_body(b); - res - }) - } - } - .map(|mut res| { - res.insert_header("X-KANIDM-OPID", hvalue); - res - }) -} - -/// Returns a generic robots.txt blocking all bots -async fn robots_txt(_req: tide::Request) -> tide::Result { - let mut res = tide::Response::new(200); - - res.set_content_type("text/plain;charset=utf-8"); - res.set_body( - r#" -User-agent: * -Disallow: / -"#, - ); - Ok(res) -} - -/// The web UI at / for Kanidm -async fn index_view(req: tide::Request) -> tide::Result { - let mut res = tide::Response::new(200); - let (eventid, hvalue) = req.new_eventid(); - - let domain_display_name = req.state().qe_r_ref.get_domain_display_name(eventid).await; - res.insert_header("X-KANIDM-OPID", hvalue); - - res.set_content_type("text/html;charset=utf-8"); - // this feels icky but I felt that adding a trait on Vec which generated the string was going a bit far - let jsfiles: Vec = req - .state() - .to_owned() - .js_files - .into_iter() - .map(|j| j.as_tag()) - .collect(); - let jstags = jsfiles.join(" "); - res.set_body(format!(r#" - - - - - - - {} - - - - - - - - - - - {} - - - -
-
- -

Kanidm is loading, please wait...

-
-
- - -"#, - domain_display_name.as_str(), - jstags, - ) - ); - - Ok(res) -} - -/// Generates the integrity hash for a file based on a filename -pub fn generate_integrity_hash(filename: String) -> Result { - let wasm_filepath = PathBuf::from(filename); - match wasm_filepath.exists() { - false => Err(format!( - "Can't find {:?} to generate file hash", - &wasm_filepath - )), - true => { - let filecontents = match std::fs::read(&wasm_filepath) { - Ok(value) => value, - Err(error) => { - return Err(format!( - "Failed to read {:?}, skipping: {:?}", - wasm_filepath, error - )); - } - }; - let shasum = - openssl::hash::hash(openssl::hash::MessageDigest::sha384(), &filecontents).unwrap(); - Ok(format!("sha384-{}", openssl::base64::encode_block(&shasum))) - } - } -} - -pub async fn create_https_server( - address: String, - domain: &String, - opt_tls_params: Option<&TlsConfiguration>, - role: ServerRole, - trust_x_forward_for: bool, - cookie_key: &[u8; 32], - jws_signer: JwsSigner, - status_ref: &'static StatusActor, - qe_w_ref: &'static QueryServerWriteV1, - qe_r_ref: &'static QueryServerReadV1, - mut rx: broadcast::Receiver, -) -> Result, ()> { - let jws_validator = jws_signer.get_validator().map_err(|e| { - error!(?e, "Failed to get jws validator"); - })?; - - let jws_validator = std::sync::Arc::new(jws_validator); - let jws_signer = std::sync::Arc::new(jws_signer); - let mut routemap = RouteMap::default(); - +pub fn get_js_files(role: ServerRole) -> Vec { let mut js_files: Vec = Vec::new(); if !matches!(role, ServerRole::WriteReplicaNoUI) { // let's set up the list of js module hashes { let filepath = "wasmloader.js"; + #[allow(clippy::unwrap_used)] js_files.push(JavaScriptFile { filepath, hash: generate_integrity_hash(format!( @@ -417,6 +115,7 @@ pub async fn create_https_server( // let's set up the list of non-module hashes { let filepath = "external/bootstrap.bundle.min.js"; + #[allow(clippy::unwrap_used)] js_files.push(JavaScriptFile { filepath, hash: generate_integrity_hash(format!( @@ -429,568 +128,311 @@ pub async fn create_https_server( }); } }; + js_files +} - let mut tserver = tide::Server::with_state(AppState { +pub async fn create_https_server( + config: Configuration, + // trust_x_forward_for: bool, // TODO: #1787 make XFF headers work + cookie_key: [u8; 32], + jws_signer: JwsSigner, + status_ref: &'static StatusActor, + qe_w_ref: &'static QueryServerWriteV1, + qe_r_ref: &'static QueryServerReadV1, + mut rx: broadcast::Receiver, +) -> Result, ()> { + let jws_validator = jws_signer.get_validator().map_err(|e| { + error!(?e, "Failed to get jws validator"); + })?; + + let js_files = get_js_files(config.role); + // set up the CSP headers + // script-src 'self' + // 'sha384-Zao7ExRXVZOJobzS/uMp0P1jtJz3TTqJU4nYXkdmsjpiVD+/wcwCyX7FGqRIqvIz' + // 'sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM' + // 'unsafe-eval'; + let js_directives = js_files + .clone() + .into_iter() + .map(|f| f.hash) + .collect::>(); + let mut js_directives: Vec = js_directives + .into_iter() + .map(|value| CspValue::Sha384 { value }) + .collect(); + js_directives.extend(vec![CspValue::UnsafeEval, CspValue::SelfSite]); + + let csp_header = axum_csp::CspSetBuilder::new() + // default-src 'self'; + .add(CspDirectiveType::DefaultSrc, vec![CspValue::SelfSite]) + // form-action https: 'self'; + .add( + CspDirectiveType::FormAction, + vec![CspValue::SelfSite, CspValue::SchemeHttps], + ) + // base-uri 'self'; + .add( + CspDirectiveType::BaseUri, + vec![CspValue::SelfSite, CspValue::SchemeHttps], + ) + // worker-src 'none'; + .add(CspDirectiveType::WorkerSource, vec![CspValue::None]) + // frame-ancestors 'none' + .add(CspDirectiveType::FrameAncestors, vec![CspValue::None]) + .add(CspDirectiveType::ScriptSource, js_directives) + .add( + CspDirectiveType::ImgSrc, + vec![CspValue::SelfSite, CspValue::SchemeData], + ); + + let store = async_session::CookieStore::new(); + let secret = format!("{:?}", cookie_key); + let secret = secret.as_bytes(); // TODO the cookie/session secret needs to be longer? + let session_layer = SessionLayer::new(store, secret) + .with_cookie_name("kanidm-session") + .with_session_ttl(None) + .with_cookie_domain(config.domain) + .with_same_site_policy(SameSite::Strict) + .with_secure(true); + + let state = ServerState { status_ref, qe_w_ref, qe_r_ref, jws_signer, jws_validator, - js_files: js_files.to_owned(), - trust_x_forward_for, - }); - - // Add the logging subsystem. - tserver.with(sketching::middleware::TreeMiddleware::new( - trust_x_forward_for, - )); - - // Add cookie handling. - tserver.with( - // We do not force a session ttl, because we validate this elsewhere in usage. - tide::sessions::SessionMiddleware::new(tide::sessions::CookieStore::new(), cookie_key) - .with_session_ttl(None) - .with_cookie_name("kanidm-session") - // Without this, the cookies won't be used on subdomains of origin. - .with_cookie_domain(domain) - // Im not sure if we need Lax here, I don't think we do because on the first get - // we don't need the cookie since wasm drives the fetches. - .with_same_site_policy(tide::http::cookies::SameSite::Strict), - ); - - // Strict responses. - tserver.with(StrictResponseMiddleware::default()); - - // Add routes - // ==== static content routes that have a longer cache policy. - - // If we are no-ui, we remove this. - if !matches!(role, ServerRole::WriteReplicaNoUI) { - let pkg_path = PathBuf::from(env!("KANIDM_WEB_UI_PKG_PATH")); - if !pkg_path.exists() { - eprintln!( - "Couldn't find Web UI package path: ({}), quitting.", - env!("KANIDM_WEB_UI_PKG_PATH") - ); - std::process::exit(1); - } - info!("Web UI package path: {:?}", canonicalize(pkg_path).unwrap()); - - /* - Let's build a compression middleware! - - The threat of the TLS BREACH attack [1] was considered as part of adding - the CompressMiddleware configuration. - - The attack targets secrets compressed and encrypted in flight with the intent - to infer their content. - - This is not a concern for the paths covered by this configuration - ( /, /ui/, /pkg/ ), - as they're all static content with no secrets in transit - all that data should - come from Kanidm's REST API, which is on a different path and not covered by - the compression middleware. - - - [1] - https://resources.infosecinstitute.com/topic/the-breach-attack/ - */ - - let compress_middleware = CompressMiddleware::builder() - .threshold(1024) - .content_type_check(Some(compression_content_type_checker())) - .build(); - - let mut static_tserver = tserver.at(""); - static_tserver.with(StaticContentMiddleware::default()); - - static_tserver.with(UIContentSecurityPolicyResponseMiddleware::new(js_files)); - - // The compression middleware needs to be the last one added before routes - static_tserver.with(compress_middleware.clone()); - - static_tserver.at("/").mapped_get(&mut routemap, index_view); - static_tserver - .at("/robots.txt") - .mapped_get(&mut routemap, robots_txt); - static_tserver - .at("/manifest.webmanifest") - .mapped_get(&mut routemap, manifest); - static_tserver - .at("/ui/") - .mapped_get(&mut routemap, index_view); - static_tserver - .at("/ui/*") - .mapped_get(&mut routemap, index_view); - - let mut static_dir_tserver = tserver.at(""); - static_dir_tserver.with(StaticContentMiddleware::default()); - // The compression middleware needs to be the last one added before routes - static_dir_tserver.with(compress_middleware); - static_dir_tserver - .at("/pkg") - .serve_dir(env!("KANIDM_WEB_UI_PKG_PATH")) - .map_err(|e| { - error!( - "Failed to serve pkg dir {} -> {:?}", - env!("KANIDM_WEB_UI_PKG_PATH"), - e - ); - })?; + js_files, + csp_header: csp_header.finish(), }; - // ==== Some routes can be cached - these are here: - let mut tserver_cacheable = tserver.at(""); - // Add our version injector, we only add this to apis. - tserver_cacheable.with(VersionHeaderMiddleware::default()); - tserver_cacheable.with(CacheableMiddleware::default()); - - // We allow clients to cache the unix token for accounts and groups. - let mut account_route_cacheable = tserver_cacheable.at("/v1/account"); - account_route_cacheable - .at("/:id/_unix/_token") - .mapped_get(&mut routemap, account_get_id_unix_token); - // We allow caching of the radius token. - account_route_cacheable - .at("/:id/_radius/_token") - .mapped_get(&mut routemap, account_get_id_radius_token); - - let mut group_route_cacheable = tserver_cacheable.at("/v1/group"); - group_route_cacheable - .at("/:id/_unix/_token") - .mapped_get(&mut routemap, group_get_id_unix_token); - - // We allow caching oauth2 RP icons. - let mut oauth2_route_cacheable = tserver_cacheable.at("/v1/oauth2"); - oauth2_route_cacheable - .at("/:rs_name/_icon") - .mapped_get(&mut routemap, do_nothing); - - // ==== These routes can not be cached - let mut appserver = tserver.at(""); - // Add our version injector, we only add this to apis. - appserver.with(VersionHeaderMiddleware::default()); - appserver.with(NoCacheMiddleware::default()); - - // let mut well_known = appserver.at("/.well-known"); - - appserver - .at("/status") - .mapped_get(&mut routemap, self::status); - - // == oauth endpoints. - oauth2_route_setup(&mut appserver, &mut routemap); - - // == scim endpoints. - scim_route_setup(&mut appserver, &mut routemap); - - let mut raw_route = appserver.at("/v1/raw"); - raw_route.at("/create").mapped_post(&mut routemap, create); - raw_route.at("/modify").mapped_post(&mut routemap, modify); - raw_route.at("/delete").mapped_post(&mut routemap, delete); - raw_route.at("/search").mapped_post(&mut routemap, search); - - appserver.at("/v1/auth").mapped_post(&mut routemap, auth); - appserver - .at("/v1/auth/valid") - .mapped_get(&mut routemap, auth_valid); - appserver - .at("/v1/reauth") - .mapped_post(&mut routemap, reauth); - - appserver.at("/v1/logout").mapped_get(&mut routemap, logout); - - let mut schema_route = appserver.at("/v1/schema"); - schema_route.at("/").mapped_get(&mut routemap, schema_get); - schema_route - .at("/attributetype") - .mapped_get(&mut routemap, schema_attributetype_get) - .mapped_post(&mut routemap, do_nothing); - schema_route - .at("/attributetype/:id") - .mapped_get(&mut routemap, schema_attributetype_get_id) - .mapped_put(&mut routemap, do_nothing) - .mapped_patch(&mut routemap, do_nothing); - - schema_route - .at("/classtype") - .mapped_get(&mut routemap, schema_classtype_get) - .mapped_post(&mut routemap, do_nothing); - schema_route - .at("/classtype/:id") - .mapped_get(&mut routemap, schema_classtype_get_id) - .mapped_put(&mut routemap, do_nothing) - .mapped_patch(&mut routemap, do_nothing); - - let mut oauth2_route = appserver.at("/v1/oauth2"); - oauth2_route.at("/").mapped_get(&mut routemap, oauth2_get); - - oauth2_route - .at("/_basic") - .mapped_post(&mut routemap, oauth2_basic_post); - - oauth2_route - .at("/:rs_name") - .mapped_get(&mut routemap, oauth2_id_get) - // It's not really possible to replace this wholesale. - // .mapped_put(&mut routemap, oauth2_id_put) - .mapped_patch(&mut routemap, oauth2_id_patch) - .mapped_delete(&mut routemap, oauth2_id_delete); - - oauth2_route - .at("/:rs_name/_basic_secret") - .mapped_get(&mut routemap, oauth2_id_get_basic_secret); - - oauth2_route - .at("/:id/_scopemap/:group") - .mapped_post(&mut routemap, oauth2_id_scopemap_post) - .mapped_delete(&mut routemap, oauth2_id_scopemap_delete); - - oauth2_route - .at("/:id/_sup_scopemap/:group") - .mapped_post(&mut routemap, oauth2_id_sup_scopemap_post) - .mapped_delete(&mut routemap, oauth2_id_sup_scopemap_delete); - - let mut self_route = appserver.at("/v1/self"); - self_route.at("/").mapped_get(&mut routemap, whoami); - self_route.at("/_uat").mapped_get(&mut routemap, whoami_uat); - - self_route - .at("/_attr/:attr") - .mapped_get(&mut routemap, do_nothing); - self_route - .at("/_credential") - .mapped_get(&mut routemap, do_nothing); - - self_route - .at("/_credential/:cid/_lock") - .mapped_get(&mut routemap, do_nothing); - - self_route - .at("/_radius") - .mapped_get(&mut routemap, do_nothing) - .mapped_delete(&mut routemap, do_nothing) - .mapped_post(&mut routemap, do_nothing); - - self_route - .at("/_radius/_config") - .mapped_post(&mut routemap, do_nothing); - self_route - .at("/_radius/_config/:token") - .mapped_get(&mut routemap, do_nothing); - self_route - .at("/_radius/_config/:token/apple") - .mapped_get(&mut routemap, do_nothing); - - // Applinks are the list of apps this account can access. - self_route - .at("/_applinks") - .mapped_get(&mut routemap, applinks_get); - - let mut person_route = appserver.at("/v1/person"); - person_route - .at("/") - .mapped_get(&mut routemap, person_get) - .mapped_post(&mut routemap, person_post); - person_route - .at("/:id") - .mapped_get(&mut routemap, person_id_get) - .mapped_patch(&mut routemap, account_id_patch) - .mapped_delete(&mut routemap, person_account_id_delete); - person_route - .at("/:id/_attr/:attr") - .mapped_get(&mut routemap, account_id_get_attr) - .mapped_put(&mut routemap, account_id_put_attr) - .mapped_post(&mut routemap, account_id_post_attr) - .mapped_delete(&mut routemap, account_id_delete_attr); - - person_route - .at("/:id/_lock") - .mapped_get(&mut routemap, do_nothing); - person_route - .at("/:id/_credential") - .mapped_get(&mut routemap, do_nothing); - person_route - .at("/:id/_credential/_status") - .mapped_get(&mut routemap, account_get_id_credential_status); - person_route - .at("/:id/_credential/:cid/_lock") - .mapped_get(&mut routemap, do_nothing); - person_route - .at("/:id/_credential/_update") - .mapped_get(&mut routemap, account_get_id_credential_update); - person_route - .at("/:id/_credential/_update_intent") - .mapped_get(&mut routemap, account_get_id_credential_update_intent); - person_route - .at("/:id/_credential/_update_intent/:ttl") - .mapped_get(&mut routemap, account_get_id_credential_update_intent); - - person_route - .at("/:id/_ssh_pubkeys") - .mapped_get(&mut routemap, account_get_id_ssh_pubkeys) - .mapped_post(&mut routemap, account_post_id_ssh_pubkey); - person_route - .at("/:id/_ssh_pubkeys/:tag") - .mapped_get(&mut routemap, account_get_id_ssh_pubkey_tag) - .mapped_delete(&mut routemap, account_delete_id_ssh_pubkey_tag); - - person_route - .at("/:id/_radius") - .mapped_get(&mut routemap, account_get_id_radius) - .mapped_post(&mut routemap, account_post_id_radius_regenerate) - .mapped_delete(&mut routemap, account_delete_id_radius); - - person_route - .at("/:id/_unix") - .mapped_post(&mut routemap, account_post_id_unix); - person_route - .at("/:id/_unix/_credential") - .mapped_put(&mut routemap, account_put_id_unix_credential) - .mapped_delete(&mut routemap, account_delete_id_unix_credential); - - // Service accounts - - let mut service_account_route = appserver.at("/v1/service_account"); - service_account_route - .at("/") - .mapped_get(&mut routemap, service_account_get) - .mapped_post(&mut routemap, service_account_post); - service_account_route - .at("/:id") - .mapped_get(&mut routemap, service_account_id_get) - .mapped_patch(&mut routemap, account_id_patch) - .mapped_delete(&mut routemap, service_account_id_delete); - service_account_route - .at("/:id/_attr/:attr") - .mapped_get(&mut routemap, account_id_get_attr) - .mapped_put(&mut routemap, account_id_put_attr) - .mapped_post(&mut routemap, account_id_post_attr) - .mapped_delete(&mut routemap, account_id_delete_attr); - - service_account_route - .at("/:id/_lock") - .mapped_get(&mut routemap, do_nothing); - - service_account_route - .at("/:id/_into_person") - .mapped_post(&mut routemap, service_account_into_person); - - service_account_route - .at("/:id/_api_token") - .mapped_post(&mut routemap, service_account_api_token_post) - .mapped_get(&mut routemap, service_account_api_token_get); - service_account_route - .at("/:id/_api_token/:token_id") - .mapped_delete(&mut routemap, service_account_api_token_delete); - - service_account_route - .at("/:id/_credential") - .mapped_get(&mut routemap, do_nothing); - service_account_route - .at("/:id/_credential/_generate") - .mapped_get(&mut routemap, service_account_credential_generate); - service_account_route - .at("/:id/_credential/_status") - .mapped_get(&mut routemap, account_get_id_credential_status); - service_account_route - .at("/:id/_credential/:cid/_lock") - .mapped_get(&mut routemap, do_nothing); - - service_account_route - .at("/:id/_ssh_pubkeys") - .mapped_get(&mut routemap, account_get_id_ssh_pubkeys) - .mapped_post(&mut routemap, account_post_id_ssh_pubkey); - service_account_route - .at("/:id/_ssh_pubkeys/:tag") - .mapped_get(&mut routemap, account_get_id_ssh_pubkey_tag) - .mapped_delete(&mut routemap, account_delete_id_ssh_pubkey_tag); - - service_account_route - .at("/:id/_unix") - .mapped_post(&mut routemap, account_post_id_unix); - - // Shared account features only - mainly this is for unix-like - // features. - let mut account_route = appserver.at("/v1/account"); - account_route - .at("/:id/_unix/_auth") - .mapped_post(&mut routemap, account_post_id_unix_auth); - account_route - .at("/:id/_ssh_pubkeys") - .mapped_get(&mut routemap, account_get_id_ssh_pubkeys); - account_route - .at("/:id/_ssh_pubkeys/:tag") - .mapped_get(&mut routemap, account_get_id_ssh_pubkey_tag); - account_route - .at("/:id/_user_auth_token") - .mapped_get(&mut routemap, account_get_id_user_auth_token); - account_route - .at("/:id/_user_auth_token/:token_id") - .mapped_delete(&mut routemap, account_user_auth_token_delete); - - // Credential updates, don't require the account id. - let mut cred_route = appserver.at("/v1/credential"); - cred_route - .at("/_exchange_intent") - .mapped_post(&mut routemap, credential_update_exchange_intent); - - cred_route - .at("/_status") - .mapped_post(&mut routemap, credential_update_status); - - cred_route - .at("/_update") - .mapped_post(&mut routemap, credential_update_update); - - cred_route - .at("/_commit") - .mapped_post(&mut routemap, credential_update_commit); - - cred_route - .at("/_cancel") - .mapped_post(&mut routemap, credential_update_cancel); - - let mut group_route = appserver.at("/v1/group"); - group_route - .at("/") - .mapped_get(&mut routemap, group_get) - .mapped_post(&mut routemap, group_post); - group_route - .at("/:id") - .mapped_get(&mut routemap, group_id_get) - .mapped_delete(&mut routemap, group_id_delete); - group_route - .at("/:id/_attr/:attr") - .mapped_delete(&mut routemap, group_id_delete_attr) - .mapped_get(&mut routemap, group_id_get_attr) - .mapped_put(&mut routemap, group_id_put_attr) - .mapped_post(&mut routemap, group_id_post_attr); - group_route - .at("/:id/_unix") - .mapped_post(&mut routemap, group_post_id_unix); - - let mut domain_route = appserver.at("/v1/domain"); - domain_route.at("/").mapped_get(&mut routemap, domain_get); - domain_route - .at("/_attr/:attr") - .mapped_get(&mut routemap, domain_get_attr) - .mapped_put(&mut routemap, domain_put_attr) - .mapped_delete(&mut routemap, domain_delete_attr); - - let mut system_route = appserver.at("/v1/system"); - system_route.at("/").mapped_get(&mut routemap, system_get); - system_route - .at("/_attr/:attr") - .mapped_get(&mut routemap, system_get_attr) - .mapped_post(&mut routemap, system_post_attr) - .mapped_delete(&mut routemap, system_delete_attr); - - let mut recycle_route = appserver.at("/v1/recycle_bin"); - recycle_route - .at("/") - .mapped_get(&mut routemap, recycle_bin_get); - recycle_route - .at("/:id") - .mapped_get(&mut routemap, recycle_bin_id_get); - recycle_route - .at("/:id/_revive") - .mapped_post(&mut routemap, recycle_bin_revive_id_post); - - let mut accessprof_route = appserver.at("/v1/access_profile"); - accessprof_route - .at("/") - .mapped_get(&mut routemap, do_nothing); - accessprof_route - .at("/:id") - .mapped_get(&mut routemap, do_nothing); - accessprof_route - .at("/:id/_attr/:attr") - .mapped_get(&mut routemap, do_nothing); - - routemap.push_self("/v1/routemap".to_string(), http_types::Method::Get); - appserver.at("/v1/routemap").nest({ - let mut route_api = tide::with_state(routemap); - route_api.at("/").get(do_routemap); - route_api - }); - // routemap_route.at("/").mapped_get(&mut routemap, do_routemap); - // === End routes - - let handle = match opt_tls_params { - Some(tls_param) => { - let tlsl = TlsListener::build() - .addrs(&address) - .cert(&tls_param.chain) - .key(&tls_param.key) - .finish() - .map_err(|e| { - error!("Failed to build TLS Listener -> {:?}", e); - })?; - - let mut listener = tlsl.to_listener().map_err(|e| { - error!("Failed to convert to Listener -> {:?}", e); - })?; - - if let Err(e) = listener.bind(tserver).await { - error!( - "Failed to start server listener on address {:?} -> {:?}", - &address, e - ); - return Err(()); - } - - tokio::spawn(async move { - tokio::select! { - Ok(action) = rx.recv() => { - match action { - CoreAction::Shutdown => {}, - } - } - server_result = listener.accept() => { - if let Err(e) = server_result { - error!( - "Failed to accept via listener on address {:?} -> {:?}", - &address, e - ); - } - } - }; - info!("Stopped HTTPSAcceptorActor"); - }) + let static_routes = match config.role { + ServerRole::WriteReplica | ServerRole::ReadOnlyReplica => { + Router::new() + // direct users to the login page + .route("/", get(|| async { Redirect::temporary("/ui/login") })) + .route("/ui/", get(crate::https::ui::ui_handler)) + // matches /ui/* but adds a path var `key` if you really wanted to capture it later. + .route("/ui/*key", get(crate::https::ui::ui_handler)) + .route("/manifest.webmanifest", get(manifest::manifest)) + .layer(middleware::compression::new()) // TODO: this needs to be configured properly } - None => { - // Create without https - let mut listener = (&address).to_listener().map_err(|e| { - error!("Failed to convert to Listener -> {:?}", e); - })?; + ServerRole::WriteReplicaNoUI => Router::new(), + }; - if let Err(e) = listener.bind(tserver).await { - error!( - "Failed to start server listener on address {:?} -> {:?}", - &address, e + // // == oauth endpoints. + // TODO: turn this from a nest into a merge because state things are bad in nested routes + let app = Router::new() + .nest("/oauth2", oauth2::oauth2_route_setup(state.clone())) + .nest("/scim", v1_scim::scim_route_setup()) + .route("/robots.txt", get(robots_txt)) + .nest("/v1", v1::router(state.clone())) + // Shared account features only - mainly this is for unix-like features. + .route("/status", get(status)); + let app = match config.role { + ServerRole::WriteReplicaNoUI => app, + ServerRole::WriteReplica | ServerRole::ReadOnlyReplica => { + let pkg_path = PathBuf::from(env!("KANIDM_WEB_UI_PKG_PATH")); + if !pkg_path.exists() { + eprintln!( + "Couldn't find Web UI package path: ({}), quitting.", + env!("KANIDM_WEB_UI_PKG_PATH") ); - return Err(()); + std::process::exit(1); } + app.nest_service("/pkg", ServeDir::new(pkg_path)) + } + }; - tokio::spawn(async move { - tokio::select! { - Ok(action) = rx.recv() => { - match action { - CoreAction::Shutdown => {}, - } - } - server_result = listener.accept() => { - if let Err(e) = server_result { - error!( - "Failed to accept via listener on address {:?} -> {:?}", - &address, e - ); - } - } + let app = app + .merge(static_routes) + .layer(from_fn_with_state( + state.clone(), + middleware::csp_headers::cspheaders_layer, + )) + .layer(from_fn(middleware::version_middleware)) + .layer(from_fn(middleware::kopid_end)) + .layer(from_fn(middleware::kopid_start)) + .layer(session_layer) + .with_state(state) + .layer(TraceLayer::new_for_http()) + // the connect_info bit here lets us pick up the remote address of the client + .into_make_service_with_connect_info::(); + + let addr = SocketAddr::from_str(&config.address).map_err(|err| { + error!( + "Failed to parse address ({:?}) from config: {:?}", + config.address, err + ); + })?; + + info!("Starting the web server..."); + + Ok(tokio::spawn(async move { + tokio::select! { + Ok(action) = rx.recv() => { + match action { + CoreAction::Shutdown => {}, } - info!("Stopped HTTPAcceptorActor"); - }) - } - }; - Ok(handle) + } + res = match config.tls_config { + Some(tls_param) => { + tokio::spawn(server_loop(tls_param, addr, app)) + }, + None => { + tokio::spawn(axum_server::bind(addr).serve(app)) + } + } => { + if let Err(err) = res { + error!("Web server exited with {:?}", err); + } + } + }; + #[cfg(feature = "otel")] + opentelemetry::global::shutdown_tracer_provider(); + info!("Stopped WebAcceptorActor"); + })) +} + +async fn server_loop( + tls_param: TlsConfiguration, + addr: SocketAddr, + app: IntoMakeServiceWithConnectInfo, +) -> Result<(), std::io::Error> { + let mut tls_builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls())?; + let mut app = app; + tls_builder + .set_certificate_file(tls_param.chain.clone(), SslFiletype::PEM) + .map_err(|err| { + std::io::Error::new( + ErrorKind::Other, + format!("Failed to create TLS listener: {:?}", err), + ) + })?; + tls_builder + .set_private_key_file(tls_param.key.clone(), SslFiletype::PEM) + .map_err(|err| { + std::io::Error::new( + ErrorKind::Other, + format!("Failed to create TLS listener: {:?}", err), + ) + })?; + tls_builder.check_private_key().map_err(|err| { + std::io::Error::new( + ErrorKind::Other, + format!("Failed to create TLS listener: {:?}", err), + ) + })?; + let acceptor = tls_builder.build(); + let listener = TcpListener::bind(addr).await?; + + let protocol = Arc::new(Http::new()); + let mut listener = + hyper::server::conn::AddrIncoming::from_listener(listener).map_err(|err| { + std::io::Error::new( + ErrorKind::Other, + format!("Failed to create listener: {:?}", err), + ) + })?; + loop { + if let Some(Ok(stream)) = poll_fn(|cx| Pin::new(&mut listener).poll_accept(cx)).await { + let acceptor = acceptor.clone(); + let svc = tower::MakeService::make_service(&mut app, &stream); + tokio::spawn(handle_conn(acceptor, stream, svc, protocol.clone())); + } + } +} + +// #[instrument(name = "handle-connection", level = "debug", skip_all)] +/// This handles an individual connection. +async fn handle_conn( + acceptor: SslAcceptor, + stream: AddrStream, + svc: ResponseFuture, + protocol: Arc, +) -> Result<(), std::io::Error> { + let ssl = Ssl::new(acceptor.context()).map_err(|e| { + error!("Failed to create TLS context: {:?}", e); + std::io::Error::from(ErrorKind::ConnectionAborted) + })?; + + let mut tls_stream = SslStream::new(ssl, stream).map_err(|e| { + error!("Failed to create TLS stream: {:?}", e); + std::io::Error::from(ErrorKind::ConnectionAborted) + })?; + + match SslStream::accept(Pin::new(&mut tls_stream)).await { + Ok(_) => { + let svc = svc.await.map_err(|e| { + error!("Failed to build HTTP response: {:?}", e); + std::io::Error::from(ErrorKind::Other) + })?; + + protocol + .serve_connection(tls_stream, svc) + .await + .map_err(|e| { + error!("Failed to complete connection: {:?}", e); + std::io::Error::from(ErrorKind::ConnectionAborted) + }) + } + Err(_error) => { + // trace!("Failed to handle connection: {:?}", error); + Ok(()) + } + } +} + +/// Convert any kind of Result into an axum response with a stable type +/// by JSON-encoding the body. +#[instrument(name = "to_axum_response", level = "debug")] +pub fn to_axum_response( + v: Result, +) -> Response { + match v { + Ok(iv) => { + let body = match serde_json::to_string(&iv) { + Ok(val) => val, + Err(err) => { + error!("Failed to serialise response: {:?}", err); + format!("{:?}", iv) + } + }; + trace!("Response Body: {:?}", body); + #[allow(clippy::unwrap_used)] + Response::builder().body(Body::from(body)).unwrap() + } + Err(e) => { + debug!("OperationError: {:?}", e); + let res = match &e { + OperationError::NotAuthenticated | OperationError::SessionExpired => { + // https://datatracker.ietf.org/doc/html/rfc7235#section-4.1 + Response::builder() + .status(http::StatusCode::UNAUTHORIZED) + .header("WWW-Authenticate", "Bearer") + } + OperationError::SystemProtectedObject | OperationError::AccessDenied => { + Response::builder().status(http::StatusCode::FORBIDDEN) + } + OperationError::NoMatchingEntries => { + Response::builder().status(http::StatusCode::NOT_FOUND) + } + OperationError::PasswordQuality(_) + | OperationError::EmptyRequest + | OperationError::SchemaViolation(_) => { + Response::builder().status(http::StatusCode::BAD_REQUEST) + } + _ => Response::builder().status(http::StatusCode::INTERNAL_SERVER_ERROR), + }; + match serde_json::to_string(&e) { + #[allow(clippy::expect_used)] + Ok(val) => res + .body(Body::from(val)) + .expect("Failed to build response!"), + #[allow(clippy::expect_used)] + Err(_) => res + .body(Body::from(format!("{:?}", e))) + .expect("Failed to build response!"), + } + } + } } diff --git a/server/core/src/https/oauth2.rs b/server/core/src/https/oauth2.rs index df1e025bf..43b9bafa1 100644 --- a/server/core/src/https/oauth2.rs +++ b/server/core/src/https/oauth2.rs @@ -1,194 +1,184 @@ +use super::middleware::KOpId; +use super::v1::{json_rest_event_get, json_rest_event_post}; +use super::{to_axum_response, ServerState}; +use axum::extract::{Path, Query, State}; +use axum::middleware::from_fn; +use axum::response::{IntoResponse, Response}; +use axum::routing::{get, post}; +use axum::{Extension, Form, Json, Router}; +use http::header::{ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION, LOCATION}; +use http::{HeaderMap, HeaderValue, StatusCode}; +use hyper::Body; use kanidm_proto::oauth2::AuthorisationResponse; use kanidm_proto::v1::Entry as ProtoEntry; use kanidmd_lib::idm::oauth2::{ AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess, AuthoriseResponse, ErrorResponse, Oauth2Error, TokenRevokeRequest, }; +use kanidmd_lib::prelude::f_eq; use kanidmd_lib::prelude::*; +use kanidmd_lib::value::PartialValue; use serde::{Deserialize, Serialize}; -use super::routemaps::{RouteMap, RouteMaps}; -use super::v1::{json_rest_event_get, json_rest_event_post}; -use super::{to_tide_response, AppState, RequestExtensions}; - // == Oauth2 Configuration Endpoints == -pub async fn oauth2_get(req: tide::Request) -> tide::Result { +/// List all the OAuth2 Resource Servers +pub async fn oauth2_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq( "class", PartialValue::new_class("oauth2_resource_server") )); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } -pub async fn oauth2_basic_post(req: tide::Request) -> tide::Result { +pub async fn oauth2_basic_post( + State(state): State, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { let classes = vec![ "oauth2_resource_server".to_string(), "oauth2_resource_server_basic".to_string(), "object".to_string(), ]; - json_rest_event_post(req, classes).await + json_rest_event_post(state, classes, obj, kopid).await } -fn oauth2_id(id: &str) -> Filter { +fn oauth2_id(rs_name: &str) -> Filter { filter_all!(f_and!([ f_eq("class", PartialValue::new_class("oauth2_resource_server")), - f_eq("oauth2_rs_name", PartialValue::new_iname(id)) + f_eq("oauth2_rs_name", PartialValue::new_iname(rs_name)) ])) } -pub async fn oauth2_id_get(req: tide::Request) -> tide::Result { - // Get a specific config - let uat = req.get_current_uat(); - let id = req.get_url_param("rs_name")?; +pub async fn oauth2_id_get( + State(state): State, + Path(rs_name): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { - let filter = oauth2_id(&id); + let filter = oauth2_id(&rs_name); - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_r_ref - .handle_internalsearch(uat, filter, None, eventid) + .handle_internalsearch(kopid.uat, filter, None, kopid.eventid) .await .map(|mut r| r.pop()); - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn oauth2_id_get_basic_secret(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - - let id = req.get_url_param("rs_name")?; - let filter = oauth2_id(&id); - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +#[instrument(level = "info", skip(state))] +pub async fn oauth2_id_get_basic_secret( + State(state): State, + Extension(kopid): Extension, + Path(rs_name): Path, +) -> impl IntoResponse { + let filter = oauth2_id(&rs_name); + let res = state .qe_r_ref - .handle_oauth2_basic_secret_read(uat, filter, eventid) + .handle_oauth2_basic_secret_read(kopid.uat, filter, kopid.eventid) .await; - - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn oauth2_id_patch(mut req: tide::Request) -> tide::Result { - // Update a value / attrs - let uat = req.get_current_uat(); - let id = req.get_url_param("rs_name")?; +pub async fn oauth2_id_patch( + State(state): State, + Path(rs_name): Path, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { + let filter = oauth2_id(&rs_name); - let obj: ProtoEntry = req.body_json().await?; - - let filter = oauth2_id(&id); - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_w_ref - .handle_internalpatch(uat, filter, obj, eventid) + .handle_internalpatch(kopid.uat, filter, obj, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn oauth2_id_scopemap_post(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - let group = req.get_url_param("group")?; - - let scopes: Vec = req.body_json().await?; - - let filter = oauth2_id(&id); - - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() +pub async fn oauth2_id_scopemap_post( + State(state): State, + Extension(kopid): Extension, + Path((rs_name, group)): Path<(String, String)>, + Json(scopes): Json>, +) -> impl IntoResponse { + let filter = oauth2_id(&rs_name); + let res = state .qe_w_ref - .handle_oauth2_scopemap_update(uat, group, scopes, filter, eventid) + .handle_oauth2_scopemap_update(kopid.uat, group, scopes, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn oauth2_id_scopemap_delete(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - let group = req.get_url_param("group")?; - - let filter = oauth2_id(&id); - - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() +pub async fn oauth2_id_scopemap_delete( + State(state): State, + Extension(kopid): Extension, + Path((rs_name, group)): Path<(String, String)>, +) -> impl IntoResponse { + let filter = oauth2_id(&rs_name); + let res = state .qe_w_ref - .handle_oauth2_scopemap_delete(uat, group, filter, eventid) + .handle_oauth2_scopemap_delete(kopid.uat, group, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn oauth2_id_sup_scopemap_post(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - let group = req.get_url_param("group")?; - - let scopes: Vec = req.body_json().await?; - - let filter = oauth2_id(&id); - - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() +pub async fn oauth2_id_sup_scopemap_post( + State(state): State, + Extension(kopid): Extension, + Path((rs_name, group)): Path<(String, String)>, + Json(scopes): Json>, +) -> impl IntoResponse { + let filter = oauth2_id(&rs_name); + let res = state .qe_w_ref - .handle_oauth2_sup_scopemap_update(uat, group, scopes, filter, eventid) + .handle_oauth2_sup_scopemap_update(kopid.uat, group, scopes, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn oauth2_id_sup_scopemap_delete(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - let group = req.get_url_param("group")?; - - let filter = oauth2_id(&id); - - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() +pub async fn oauth2_id_sup_scopemap_delete( + State(state): State, + Extension(kopid): Extension, + Path((rs_name, group)): Path<(String, String)>, +) -> impl IntoResponse { + let filter = oauth2_id(&rs_name); + let res = state .qe_w_ref - .handle_oauth2_sup_scopemap_delete(uat, group, filter, eventid) + .handle_oauth2_sup_scopemap_delete(kopid.uat, group, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn oauth2_id_delete(req: tide::Request) -> tide::Result { - // Delete this - let uat = req.get_current_uat(); - let id = req.get_url_param("rs_name")?; - - let filter = oauth2_id(&id); - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn oauth2_id_delete( + State(state): State, + Extension(kopid): Extension, + Path(rs_name): Path, +) -> impl IntoResponse { + let filter = oauth2_id(&rs_name); + let res = state .qe_w_ref - .handle_internaldelete(uat, filter, eventid) + .handle_internaldelete(kopid.uat, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } // == OAUTH2 PROTOCOL FLOW HANDLERS == - +// // oauth2 (partial) // https://tools.ietf.org/html/rfc6749 // oauth2 pkce // https://tools.ietf.org/html/rfc7636 - +// // TODO // oauth2 token introspection // https://tools.ietf.org/html/rfc7662 // oauth2 bearer token // https://tools.ietf.org/html/rfc6750 - +// // From https://datatracker.ietf.org/doc/html/rfc6749#section-4.1 // // +----------+ @@ -237,45 +227,40 @@ pub async fn oauth2_id_delete(req: tide::Request) -> tide::Result { // valid Kanidm instance in the topology can handle these request. // -pub async fn oauth2_authorise_post(mut req: tide::Request) -> tide::Result { - let auth_req: AuthorisationRequest = req.body_json().await?; - oauth2_authorise(req, auth_req).await.map(|mut res| { - if res.status() == 302 { - // in post, we need the redirect not to be issued, so we mask 302 to 200 - res.set_status(200); - } - res - }) + + +pub async fn oauth2_authorise_post( + State(state): State, + Extension(kopid): Extension, + Json(auth_req): Json, +) -> impl IntoResponse { + let mut res = oauth2_authorise(state, auth_req, kopid) + .await + .into_response(); + if res.status() == StatusCode::FOUND { + // in post, we need the redirect not to be issued, so we mask 302 to 200 + *res.status_mut() = StatusCode::OK; + } + res } -pub async fn oauth2_authorise_get(req: tide::Request) -> tide::Result { +pub async fn oauth2_authorise_get( + State(state): State, + Extension(kopid): Extension, + Query(auth_req): Query, +) -> impl IntoResponse { // Start the oauth2 authorisation flow to present to the user. - debug!("Request Query - {:?}", req.url().query()); - // Get the authorisation request. - let auth_req: AuthorisationRequest = req.query().map_err(|e| { - error!("{:?}", e); - tide::Error::from_str( - tide::StatusCode::BadRequest, - "Invalid Oauth2 AuthorisationRequest", - ) - })?; - - oauth2_authorise(req, auth_req).await + oauth2_authorise(state, auth_req, kopid).await } async fn oauth2_authorise( - req: tide::Request, + state: ServerState, auth_req: AuthorisationRequest, -) -> tide::Result { - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); - - // let mut redir_url = auth_req.redirect_uri.clone(); - - let res = req - .state() + kopid: KOpId, +) -> impl IntoResponse { + let res: Result = state .qe_r_ref - .handle_oauth2_authorise(uat, auth_req, eventid) + .handle_oauth2_authorise(kopid.uat.clone(), auth_req, kopid.eventid) .await; match res { @@ -287,18 +272,20 @@ async fn oauth2_authorise( }) => { // Render a redirect to the consent page for the user to interact with // to authorise this session-id - let mut res = tide::Response::new(200); // This is json so later we can expand it with better detail. - tide::Body::from_json(&AuthorisationResponse::ConsentRequested { + #[allow(clippy::unwrap_used)] + let body = serde_json::to_string(&AuthorisationResponse::ConsentRequested { client_name, scopes, pii_scopes, consent_token, }) - .map(|b| { - res.set_body(b); - res - }) + .unwrap(); + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::OK) + .body(body.into()) + .unwrap() } Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess { mut redirect_uri, @@ -307,36 +294,48 @@ async fn oauth2_authorise( })) => { // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11 // We could consider changing this to 303? - let mut res = tide::Response::new(302); + #[allow(clippy::unwrap_used)] + let body = + Body::from(serde_json::to_string(&AuthorisationResponse::Permitted).unwrap()); redirect_uri .query_pairs_mut() .clear() .append_pair("state", &state) .append_pair("code", &code); - res.insert_header("Location", redirect_uri.as_str()); - // I think the client server needs this - res.insert_header( - "Access-Control-Allow-Origin", - redirect_uri.origin().ascii_serialization(), - ); - tide::Body::from_json(&AuthorisationResponse::Permitted).map(|b| { - res.set_body(b); - res - }) + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::FOUND) + .header( + LOCATION, + HeaderValue::from_str(redirect_uri.as_str()).unwrap(), + ) + // I think the client server needs this + .header( + ACCESS_CONTROL_ALLOW_ORIGIN, + HeaderValue::from_str(&redirect_uri.origin().ascii_serialization()).unwrap(), + ) + .body(body) + .unwrap() } Err(Oauth2Error::AuthenticationRequired) => { // This will trigger our ui to auth and retry. - let mut res = tide::Response::new(tide::StatusCode::Unauthorized); - res.insert_header("WWW-Authenticate", "Bearer"); - res.insert_header("Access-Control-Allow-Origin", "*"); - Ok(res) + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header("WWW-Authenticate", HeaderValue::from_str("Bearer").unwrap()) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty()) + .unwrap() } Err(Oauth2Error::AccessDenied) => { // If scopes are not available for this account. - let mut res = tide::Response::new(tide::StatusCode::Forbidden); - res.insert_header("Access-Control-Allow-Origin", "*"); - Ok(res) + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::FORBIDDEN) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty()) + .unwrap() } /* RFC - If the request fails due to a missing, invalid, or mismatching @@ -351,67 +350,59 @@ async fn oauth2_authorise( Err(e) => { admin_error!( "Unable to authorise - Error ID: {} error: {}", - &hvalue, + &kopid.eventid_value(), &e.to_string() ); - let mut res = tide::Response::new(tide::StatusCode::BadRequest); - res.insert_header("Access-Control-Allow-Origin", "*"); - Ok(res) + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::BAD_REQUEST) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty()) + .unwrap() } } - .map(|mut res| { - res.insert_header("X-KANIDM-OPID", hvalue); - res - }) } -pub async fn oauth2_authorise_permit_post(mut req: tide::Request) -> tide::Result { - let consent_req: String = req.body_json().await?; - oauth2_authorise_permit(req, consent_req) +pub async fn oauth2_authorise_permit_post( + State(state): State, + Extension(kopid): Extension, + Json(consent_req): Json, +) -> impl IntoResponse { + let mut res = oauth2_authorise_permit(state, consent_req, kopid) .await - .map(|mut res| { - if res.status() == 302 { - // in post, we need the redirect not to be issued, so we mask 302 to 200 - res.set_status(200); - } - res - }) + .into_response(); + if res.status() == StatusCode::FOUND { + // in post, we need the redirect not to be issued, so we mask 302 to 200 + *res.status_mut() = StatusCode::OK; + } + res } #[derive(Serialize, Deserialize, Debug)] -struct ConsentRequestData { +pub struct ConsentRequestData { token: String, } -pub async fn oauth2_authorise_permit_get(req: tide::Request) -> tide::Result { +pub async fn oauth2_authorise_permit_get( + State(state): State, + Query(token): Query, + Extension(kopid): Extension, +) -> impl IntoResponse { // When this is called, this indicates consent to proceed from the user. - debug!("Request Query - {:?}", req.url().query()); - - let consent_req: ConsentRequestData = req.query().map_err(|e| { - error!("{:?}", e); - tide::Error::from_str( - tide::StatusCode::BadRequest, - "Invalid Oauth2 Consent Permit", - ) - })?; - - oauth2_authorise_permit(req, consent_req.token).await + oauth2_authorise_permit(state, token.token, kopid).await } async fn oauth2_authorise_permit( - req: tide::Request, + state: ServerState, consent_req: String, -) -> tide::Result { - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + kopid: KOpId, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_oauth2_authorise_permit(uat, consent_req, eventid) + .handle_oauth2_authorise_permit(kopid.uat, consent_req, kopid.eventid) .await; - let mut res = match res { + match res { Ok(AuthorisePermitSuccess { mut redirect_uri, state, @@ -419,19 +410,21 @@ async fn oauth2_authorise_permit( }) => { // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11 // We could consider changing this to 303? - let mut res = tide::Response::new(302); redirect_uri .query_pairs_mut() .clear() .append_pair("state", &state) .append_pair("code", &code); - res.insert_header("Location", redirect_uri.as_str()); - // I think the client server needs this - res.insert_header( - "Access-Control-Allow-Origin", - redirect_uri.origin().ascii_serialization(), - ); - res + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::FOUND) + .header("Location", redirect_uri.as_str()) + .header( + "Access-Control-Allow-Origin", + redirect_uri.origin().ascii_serialization(), + ) + .body(Body::empty()) + .unwrap() } Err(_e) => { // If an error happens in our consent flow, I think @@ -442,125 +435,138 @@ async fn oauth2_authorise_permit( // Turns out this instinct was correct: // https://www.proofpoint.com/us/blog/cloud-security/microsoft-and-github-oauth-implementation-vulnerabilities-lead-redirection // Possible to use this with a malicious client configuration to phish / spam. - let mut res = tide::Response::new(tide::StatusCode::InternalServerError); - res.insert_header("Access-Control-Allow-Origin", "*"); - res + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty()) + .unwrap() } - }; - res.insert_header("X-KANIDM-OPID", hvalue); - Ok(res) + } } // When this is called, this indicates the user has REJECTED the intent to proceed. -pub async fn oauth2_authorise_reject_post(mut req: tide::Request) -> tide::Result { - let consent_req: String = req.body_json().await?; - oauth2_authorise_reject(req, consent_req).await +pub async fn oauth2_authorise_reject_post( + State(state): State, + Extension(kopid): Extension, + Form(consent_req): Form, +) -> impl IntoResponse { + oauth2_authorise_reject(state, consent_req.token, kopid).await } -pub async fn oauth2_authorise_reject_get(req: tide::Request) -> tide::Result { - debug!("Request Query - {:?}", req.url().query()); - - let consent_req: ConsentRequestData = req.query().map_err(|e| { - error!("{:?}", e); - tide::Error::from_str( - tide::StatusCode::BadRequest, - "Invalid Oauth2 Consent Reject", - ) - })?; - - oauth2_authorise_reject(req, consent_req.token).await +pub async fn oauth2_authorise_reject_get( + State(state): State, + Extension(kopid): Extension, + Query(consent_req): Query, +) -> impl IntoResponse { + oauth2_authorise_reject(state, consent_req.token, kopid).await } -// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 -// If the user willingly rejects the authorisation, we must redirect -// with an error. +// // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 +// // If the user willingly rejects the authorisation, we must redirect +// // with an error. async fn oauth2_authorise_reject( - req: tide::Request, + state: ServerState, consent_req: String, -) -> tide::Result { + kopid: KOpId, +) -> impl IntoResponse { // Need to go back to the redir_uri // For this, we'll need to lookup where to go. - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() + let res = state .qe_r_ref - .handle_oauth2_authorise_reject(uat, consent_req, eventid) + .handle_oauth2_authorise_reject(kopid.uat, consent_req, kopid.eventid) .await; - let mut res = match res { + match res { Ok(mut redirect_uri) => { - let mut res = tide::Response::new(302); redirect_uri .query_pairs_mut() .clear() .append_pair("error", "access_denied") .append_pair("error_description", "authorisation rejected"); - res.insert_header("Location", redirect_uri.as_str()); + #[allow(clippy::unwrap_used)] + Response::builder() + .header(LOCATION, redirect_uri.as_str()) + .header( + ACCESS_CONTROL_ALLOW_ORIGIN, + redirect_uri.origin().ascii_serialization(), + ) + .body(Body::empty()) + .unwrap() // I think the client server needs this - res.insert_header( - "Access-Control-Allow-Origin", - redirect_uri.origin().ascii_serialization(), - ); - res } Err(_e) => { // If an error happens in our reject flow, I think // that we should NOT redirect to the calling application // and we need to handle that locally somehow. // This needs to be better! - let mut res = tide::Response::new(500); - res.insert_header("Access-Control-Allow-Origin", "*"); - res + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty()) + .unwrap() } - }; - res.insert_header("X-KANIDM-OPID", hvalue); - Ok(res) + } } -pub async fn oauth2_token_post(mut req: tide::Request) -> tide::Result { +pub async fn oauth2_token_post( + State(state): State, + Extension(kopid): Extension, + headers: HeaderMap, // TOOD: make this a typed basic auth header + Form(tok_req): Form, +) -> impl IntoResponse { // This is called directly by the resource server, where we then issue // the token to the caller. - let (eventid, hvalue) = req.new_eventid(); // Get the authz header (if present). In the future depending on the // type of exchanges we support, this could become an Option type. - let client_authz = req - .header("authorization") - .and_then(|hv| hv.get(0)) - .and_then(|h| h.as_str().strip_prefix("Basic ")) - .map(str::to_string); - - // Get the accessToken Request - let tok_req: AccessTokenRequest = req.body_form().await.map_err(|e| { - error!("atr parse error - {:?}", e); - tide::Error::from_str( - tide::StatusCode::BadRequest, - "Invalid Oauth2 AccessTokenRequest", - ) - })?; + let client_authz = match headers + .get("authorization") + .and_then(|hv| hv.to_str().ok()) + .and_then(|h| h.split(' ').last()) + .map(str::to_string) + { + Some(val) => val, + None => { + error!("Basic Authentication Not Provided"); + #[allow(clippy::unwrap_used)] + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from("Invalid Basic Authorisation")) + .unwrap(); + } + }; // Do we change the method/path we take here based on the type of requested // grant? Should we cease the delayed/async session update here and just opt // for a wr txn? - let res = req - .state() + let res = state .qe_w_ref - .handle_oauth2_token_exchange(client_authz, tok_req, eventid) + .handle_oauth2_token_exchange(Some(client_authz), tok_req, kopid.eventid) .await; match res { - Ok(atr) => { - let mut res = tide::Response::new(200); - tide::Body::from_json(&atr).map(|b| { - res.set_body(b); - res - }) + Ok(atr) => + { + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::OK) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(serde_json::to_string(&atr).unwrap())) + .unwrap() } - Err(Oauth2Error::AuthenticationRequired) => { - Ok(tide::Response::new(tide::StatusCode::Unauthorized)) + Err(Oauth2Error::AuthenticationRequired) => + { + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty()) + .unwrap() } Err(e) => { // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 @@ -570,147 +576,173 @@ pub async fn oauth2_token_post(mut req: tide::Request) -> tide::Result error_uri: None, }; - let mut res = tide::Response::new(400); - tide::Body::from_json(&err).map(|b| { - res.set_body(b); - res - }) + let body = match serde_json::to_string(&err) { + Ok(val) => val, + Err(e) => { + admin_warn!("Failed to serialize error response: original_error=\"{:?}\" serialization_error=\"{:?}\"", err, e); + format!("{:?}", err) + } + }; + + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::BAD_REQUEST) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(body)) + .unwrap() } } - .map(|mut res| { - res.insert_header("X-KANIDM-OPID", hvalue); - res.insert_header("Access-Control-Allow-Origin", "*"); - res - }) } -// For future openid integration -pub async fn oauth2_openid_discovery_get(req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - let client_id = req.get_url_param("client_id")?; +// // For future openid integration +pub async fn oauth2_openid_discovery_get( + State(state): State, + Path(client_id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + // let client_id = req.get_url_param("client_id")?; - let res = req - .state() + let res = state .qe_r_ref - .handle_oauth2_openid_discovery(client_id, eventid) + .handle_oauth2_openid_discovery(client_id, kopid.eventid) .await; - - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn oauth2_openid_userinfo_get(req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - let client_id = req.get_url_param("client_id")?; +pub async fn oauth2_openid_userinfo_get( + State(state): State, + Path(client_id): Path, + Extension(kopid): Extension, +) -> Response { + // The token we want to inspect is in the authorisation header. - // The token we want to inspect is in the authorisatioz header. - let client_authz = req - .header("authorization") - .and_then(|hv| hv.get(0)) - .and_then(|h| h.as_str().strip_prefix("Bearer ")) - .map(str::to_string) - .ok_or_else(|| { + let client_authz = match kopid.uat { + Some(val) => val, + None => { error!("Bearer Authentication Not Provided"); - tide::Error::from_str( - tide::StatusCode::Unauthorized, - "Invalid Bearer Authorisation", - ) - })?; + #[allow(clippy::unwrap_used)] + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from("Invalid Bearer Authorisation")) + .unwrap(); + } + }; - let res = req - .state() + let res = state .qe_r_ref - .handle_oauth2_openid_userinfo(client_id, client_authz, eventid) + .handle_oauth2_openid_userinfo(client_id, client_authz, kopid.eventid) .await; match res { Ok(uir) => { - let mut res = tide::Response::new(200); - tide::Body::from_json(&uir).map(|b| { - res.set_body(b); - res - }) + #[allow(clippy::unwrap_used)] + let body = serde_json::to_string(&uir).unwrap(); + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::OK) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(body)) + .unwrap() } Err(e) => { - // https://datatracker.ietf.org/doc/html/rfc6750#section-6.2 let err = ErrorResponse { error: e.to_string(), error_description: None, error_uri: None, }; - - let mut res = tide::Response::new(400); - tide::Body::from_json(&err).map(|b| { - res.set_body(b); - res - }) + let body = match serde_json::to_string(&err) { + Ok(val) => val, + Err(e) => { + format!("{:?}", e) + } + }; + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::BAD_REQUEST) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(body)) + .unwrap() + // https://datatracker.ietf.org/doc/html/rfc6750#section-6.2 } } - .map(|mut res| { - res.insert_header("X-KANIDM-OPID", hvalue); - res.insert_header("Access-Control-Allow-Origin", "*"); - res - }) } -pub async fn oauth2_openid_publickey_get(req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - let client_id = req.get_url_param("client_id")?; - - let res = req - .state() +pub async fn oauth2_openid_publickey_get( + State(state): State, + Path(client_id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_oauth2_openid_publickey(client_id, eventid) + .handle_oauth2_openid_publickey(client_id, kopid.eventid) .await; - - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn oauth2_token_introspect_post(mut req: tide::Request) -> tide::Result { - // This is called directly by the resource server, where we then issue - // information about this token to the caller. - let (eventid, hvalue) = req.new_eventid(); - - let client_authz = req - .header("authorization") - .and_then(|hv| hv.get(0)) - .and_then(|h| h.as_str().strip_prefix("Basic ")) - .map(str::to_string) - .ok_or_else(|| { - error!("Basic Authentication Not Provided"); - tide::Error::from_str( - tide::StatusCode::Unauthorized, - "Invalid Basic Authorisation", - ) - })?; - - // Get the introspection request, could we accept json or form? Prob needs content type here. - let intr_req: AccessTokenIntrospectRequest = req.body_form().await.map_err(|e| { - request_error!("{:?}", e); - tide::Error::from_str( - tide::StatusCode::BadRequest, - "Invalid Oauth2 AccessTokenIntrospectRequest", - ) - })?; - +/// This is called directly by the resource server, where we then issue +/// information about this token to the caller. +pub async fn oauth2_token_introspect_post( + State(state): State, + Extension(kopid): Extension, + headers: HeaderMap, + Form(intr_req): Form, +) -> impl IntoResponse { + let client_authz = match kopid.uat { + Some(val) => val, + None => { + error!("Bearer Authentication Not Provided, trying basic"); + match headers.get(AUTHORIZATION) { + Some(val) => { + // LOL THIS IS HILARIOUSLY TERRIBLE BUT WE PARSE THE RAW OK + #[allow(clippy::unwrap_used)] + val.to_str() + .unwrap() + .strip_prefix("Basic ") + .unwrap() + .to_string() + } + None => { + #[allow(clippy::unwrap_used)] + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from("Invalid Bearer Authorisation")) + .unwrap(); + } + } + } + }; request_trace!("Introspect Request - {:?}", intr_req); - let res = req - .state() + let res = state .qe_r_ref - .handle_oauth2_token_introspect(client_authz, intr_req, eventid) + .handle_oauth2_token_introspect(client_authz, intr_req, kopid.eventid) .await; match res { Ok(atr) => { - let mut res = tide::Response::new(200); - tide::Body::from_json(&atr).map(|b| { - res.set_body(b); - res - }) + let body = match serde_json::to_string(&atr) { + Ok(val) => val, + Err(e) => { + admin_warn!("Failed to serialize introspect response: original_data=\"{:?}\" serialization_error=\"{:?}\"", atr, e); + format!("{:?}", atr) + } + }; + #[allow(clippy::unwrap_used)] + Response::builder() + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(body)) + .unwrap() } Err(Oauth2Error::AuthenticationRequired) => { // This will trigger our ui to auth and retry. - Ok(tide::Response::new(tide::StatusCode::Unauthorized)) + #[allow(clippy::unwrap_used)] + Response::builder() + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .status(StatusCode::UNAUTHORIZED) + .body(Body::empty()) + .unwrap() } Err(e) => { // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 @@ -720,60 +752,68 @@ pub async fn oauth2_token_introspect_post(mut req: tide::Request) -> t error_uri: None, }; - let mut res = tide::Response::new(400); - tide::Body::from_json(&err).map(|b| { - res.set_body(b); - res - }) + let body = match serde_json::to_string(&err) { + Ok(val) => val, + Err(e) => { + format!("{:?}", e) + } + }; + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::BAD_REQUEST) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(body)) + .unwrap() } } - .map(|mut res| { - res.insert_header("X-KANIDM-OPID", hvalue); - res.insert_header("Access-Control-Allow-Origin", "*"); - res - }) } -pub async fn oauth2_token_revoke_post(mut req: tide::Request) -> tide::Result { - // This is called directly by the resource server, where we then revoke - // the token identified by this request. - let (eventid, hvalue) = req.new_eventid(); - - let client_authz = req - .header("authorization") - .and_then(|hv| hv.get(0)) - .and_then(|h| h.as_str().strip_prefix("Basic ")) - .map(str::to_string) - .ok_or_else(|| { - error!("Basic Authentication Not Provided"); - tide::Error::from_str( - tide::StatusCode::Unauthorized, - "Invalid Basic Authorisation", - ) - })?; - - // Get the introspection request, could we accept json or form? Prob needs content type here. - let intr_req: TokenRevokeRequest = req.body_form().await.map_err(|e| { - request_error!("{:?}", e); - tide::Error::from_str( - tide::StatusCode::BadRequest, - "Invalid Oauth2 TokenRevokeRequest", - ) - })?; +/// This is called directly by the resource server, where we then revoke +/// the token identified by this request. +pub async fn oauth2_token_revoke_post( + State(state): State, + Extension(kopid): Extension, + Form(intr_req): Form, +) -> impl IntoResponse { + // TODO: we should handle the session-based auth bit here I think maybe possibly there's no tests + let client_authz = match kopid.uat { + Some(val) => val, + None => + { + #[allow(clippy::unwrap_used)] + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty()) + .unwrap() + } + }; request_trace!("Revoke Request - {:?}", intr_req); - let res = req - .state() + let res = state .qe_w_ref - .handle_oauth2_token_revoke(client_authz, intr_req, eventid) + .handle_oauth2_token_revoke(client_authz, intr_req, kopid.eventid) .await; match res { - Ok(()) => Ok(tide::Response::new(200)), + Ok(()) => + { + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::OK) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty()) + .unwrap() + } Err(Oauth2Error::AuthenticationRequired) => { // This will trigger our ui to auth and retry. - Ok(tide::Response::new(tide::StatusCode::Unauthorized)) + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty()) + .unwrap() } Err(e) => { // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 @@ -782,76 +822,64 @@ pub async fn oauth2_token_revoke_post(mut req: tide::Request) -> tide: error_description: None, error_uri: None, }; - - let mut res = tide::Response::new(400); - tide::Body::from_json(&err).map(|b| { - res.set_body(b); - res - }) + #[allow(clippy::unwrap_used)] + Response::builder() + .status(StatusCode::BAD_REQUEST) + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(serde_json::to_string(&err).unwrap())) + .unwrap() } } - .map(|mut res| { - res.insert_header("X-KANIDM-OPID", hvalue); - res.insert_header("Access-Control-Allow-Origin", "*"); - res - }) } -pub fn oauth2_route_setup(appserver: &mut tide::Route<'_, AppState>, routemap: &mut RouteMap) { - let mut oauth2_process = appserver.at("/oauth2"); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/authorise") - .mapped_post(routemap, oauth2_authorise_post) - .mapped_get(routemap, oauth2_authorise_get); +pub fn oauth2_route_setup(state: ServerState) -> Router { + // this has all the openid-related routes + let openid_router = Router::new() // appserver.at("/oauth2/openid"); + // // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + .route( + "/:client_id/.well-known/openid-configuration", + get(oauth2_openid_discovery_get), + ) + // // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + .route("/:client_id/userinfo", get(oauth2_openid_userinfo_get)) + // // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + .route( + "/:client_id/public_key.jwk", + get(oauth2_openid_publickey_get), + ) + .with_state(state.clone()); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/authorise/permit") - .mapped_post(routemap, oauth2_authorise_permit_post) - .mapped_get(routemap, oauth2_authorise_permit_get); - - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/authorise/reject") - .mapped_post(routemap, oauth2_authorise_reject_post) - .mapped_get(routemap, oauth2_authorise_reject_get); - - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/token") - .mapped_post(routemap, oauth2_token_post); - - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/token/introspect") - .mapped_post(routemap, oauth2_token_introspect_post); - oauth2_process - .at("/token/revoke") - .mapped_post(routemap, oauth2_token_revoke_post); - - let mut openid_process = appserver.at("/oauth2/openid"); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - - openid_process - .at("/:client_id/.well-known/openid-configuration") - .mapped_get(routemap, oauth2_openid_discovery_get); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - - openid_process - .at("/:client_id/userinfo") - .mapped_get(routemap, oauth2_openid_userinfo_get); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - - openid_process - .at("/:client_id/public_key.jwk") - .mapped_get(routemap, oauth2_openid_publickey_get); + Router::new() //= appserver.at("/oauth2"); + .route("/", get(oauth2_get)) + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + .route( + "/authorise", + post(oauth2_authorise_post).get(oauth2_authorise_get), + ) + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + .route( + "/authorise/permit", + post(oauth2_authorise_permit_post).get(oauth2_authorise_permit_get), + ) + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + .route( + "/authorise/reject", + post(oauth2_authorise_reject_post).get(oauth2_authorise_reject_get), + ) + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + .route("/token", post(oauth2_token_post)) + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + .route("/token/introspect", post(oauth2_token_introspect_post)) + .route("/token/revoke", post(oauth2_token_revoke_post)) + .nest("/openid", openid_router) + .with_state(state) + .layer(from_fn(super::middleware::caching::dont_cache_me)) } diff --git a/server/core/src/https/routemaps.rs b/server/core/src/https/routemaps.rs deleted file mode 100644 index db2597aa2..000000000 --- a/server/core/src/https/routemaps.rs +++ /dev/null @@ -1,94 +0,0 @@ -///! Route-mapping magic for tide -/// -/// Instead of adding routes with (for example) the .post method you add them with .mapped_post, passing an instance of [RouteMap] and it'll do the rest... -use serde::{Deserialize, Serialize}; -use tide::{Endpoint, Route}; - -use crate::https::AppState; - -// Extends the tide::Route for RouteMaps, this would really be nice if it was generic :( -pub trait RouteMaps { - fn mapped_method( - &mut self, - routemap: &mut RouteMap, - method: http_types::Method, - ep: impl Endpoint, - ) -> &mut Self; - fn mapped_delete(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self; - fn mapped_get(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self; - fn mapped_patch(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self; - fn mapped_post(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self; - fn mapped_put(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self; - fn mapped_update(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self; -} - -impl RouteMaps for Route<'_, AppState> { - // add a mapped method to the list - fn mapped_method( - &mut self, - routemap: &mut RouteMap, - method: http_types::Method, - ep: impl Endpoint, - ) -> &mut Self { - // TODO: truly weird things involving ASTs and sacrifices to eldritch gods to figure out how to represent the Endpoint - - // if the path is empty then it's the root path... - let path_str = self.path().to_string(); - let path = match path_str.is_empty() { - true => String::from("/"), - false => path_str, - }; - - // debug!("Mapping route: {:?}", path); - routemap.routelist.push(RouteInfo { path, method }); - self.method(method, ep) - } - - fn mapped_delete(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self { - self.mapped_method(routemap, http_types::Method::Delete, ep) - } - - fn mapped_get(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self { - self.mapped_method(routemap, http_types::Method::Get, ep) - } - - fn mapped_patch(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self { - self.mapped_method(routemap, http_types::Method::Patch, ep) - } - - fn mapped_post(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self { - self.mapped_method(routemap, http_types::Method::Post, ep) - } - - fn mapped_put(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self { - self.mapped_method(routemap, http_types::Method::Put, ep) - } - - fn mapped_update(&mut self, routemap: &mut RouteMap, ep: impl Endpoint) -> &mut Self { - self.mapped_method(routemap, http_types::Method::Update, ep) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -/// Information about a given route -pub struct RouteInfo { - pub path: String, - pub method: http_types::Method, -} - -#[derive(Clone, Debug, Deserialize, Serialize, Default)] -pub struct RouteMap { - pub routelist: Vec, -} - -impl RouteMap { - // Serializes the object out to a pretty JSON blob - pub fn do_map(&self) -> String { - serde_json::to_string_pretty(self).unwrap() - } - - // Inject the route for the routemap endpoint - pub fn push_self(&mut self, path: String, method: http_types::Method) { - self.routelist.push(RouteInfo { path, method }); - } -} diff --git a/server/core/src/https/tests.rs b/server/core/src/https/tests.rs new file mode 100644 index 000000000..b034bbfaf --- /dev/null +++ b/server/core/src/https/tests.rs @@ -0,0 +1,23 @@ +#[test] +fn test_javscriptfile() { + // make sure it outputs what we think it does + use crate::https::JavaScriptFile; + let jsf = JavaScriptFile { + filepath: "wasmloader.js", + hash: "1234567890".to_string(), + filetype: Some("module".to_string()), + }; + assert_eq!( + jsf.as_tag(), + r#""# + ); + let jsf = JavaScriptFile { + filepath: "wasmloader.js", + hash: "1234567890".to_string(), + filetype: None, + }; + assert_eq!( + jsf.as_tag(), + r#""# + ); +} diff --git a/server/core/src/https/ui.rs b/server/core/src/https/ui.rs new file mode 100644 index 000000000..c56bc1202 --- /dev/null +++ b/server/core/src/https/ui.rs @@ -0,0 +1,65 @@ +use axum::extract::State; +use axum::http::HeaderValue; +use axum::response::Response; +use axum::Extension; + +use super::middleware::KOpId; +use super::ServerState; + +pub async fn ui_handler( + State(state): State, + Extension(kopid): Extension, +) -> Response { + let domain_display_name = state.qe_r_ref.get_domain_display_name(kopid.eventid).await; + + // this feels icky but I felt that adding a trait on Vec which generated the string was going a bit far + let jsfiles: Vec = state.js_files.into_iter().map(|j| j.as_tag()).collect(); + let jstags = jsfiles.join(" "); + + let body = format!( + r#" + + + + + + + {} + + + + + + + + + + + {} + + + +
+
+ +

Kanidm is loading, please wait...

+
+
+ + +"#, + domain_display_name.as_str(), + jstags, + ); + + let mut res = Response::new(body); + res.headers_mut().insert( + "Content-Type", + HeaderValue::from_static("text/html;charset=utf-8"), + ); + res +} diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index c4a68478b..20847da29 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -1,315 +1,350 @@ +use std::net::SocketAddr; +#[allow(unused_imports)] +// //! The V1 API things! use std::str::FromStr; -use std::time::Duration; +use axum::extract::{ConnectInfo, Path, Query, State}; +use axum::headers::{CacheControl, HeaderMapExt}; +use axum::middleware::from_fn; +use axum::response::{IntoResponse, Response}; + +use axum::routing::{delete, get, post, put}; +use axum::{Extension, Json, Router}; +use axum_macros::debug_handler; +use axum_sessions::extractors::{ReadableSession, WritableSession}; use compact_jwt::Jws; +use http::{HeaderMap, HeaderValue, StatusCode}; +use hyper::Body; use kanidm_proto::v1::{ AccountUnixExtend, ApiTokenGenerate, AuthIssueSession, AuthRequest, AuthResponse, AuthState as ProtoAuthState, CUIntentToken, CURequest, CUSessionToken, CreateRequest, - DeleteRequest, Entry as ProtoEntry, GroupUnixExtend, ModifyRequest, OperationError, - SearchRequest, SingleStringRequest, + DeleteRequest, Entry as ProtoEntry, GroupUnixExtend, ModifyRequest, SearchRequest, + SingleStringRequest, }; -use kanidmd_lib::filter::{Filter, FilterInvalid}; + use kanidmd_lib::idm::event::AuthResult; use kanidmd_lib::idm::AuthState; use kanidmd_lib::prelude::*; -use kanidmd_lib::status::StatusRequestEvent; +use kanidmd_lib::value::PartialValue; use serde::{Deserialize, Serialize}; +use uuid::Uuid; -use super::{to_tide_response, AppState, RequestExtensions, RouteMap}; +use crate::https::to_axum_response; + +use super::middleware::caching::dont_cache_me; +use super::middleware::KOpId; +use super::v1_scim::*; +use super::ServerState; #[derive(Serialize, Deserialize, Debug, Clone)] pub(crate) struct SessionId { pub sessionid: Uuid, } -pub async fn create(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); +#[debug_handler] +pub async fn create( + State(state): State, + Extension(kopid): Extension, + Json(msg): Json, +) -> Response { // parse the req to a CreateRequest - let msg: CreateRequest = req.body_json().await?; + // let msg: CreateRequest = req.body_json().await?; - let (eventid, hvalue) = req.new_eventid(); - - let res = req.state().qe_w_ref.handle_create(uat, msg, eventid).await; - to_tide_response(res, hvalue) + let res = state + .qe_w_ref + .handle_create(kopid.uat, msg, kopid.eventid) + .await; + to_axum_response(res) } -pub async fn modify(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let msg: ModifyRequest = req.body_json().await?; - let (eventid, hvalue) = req.new_eventid(); - let res = req.state().qe_w_ref.handle_modify(uat, msg, eventid).await; - to_tide_response(res, hvalue) +pub async fn v1_modify( + State(state): State, + Extension(kopid): Extension, + Json(msg): Json, +) -> Response { + let res = state + .qe_w_ref + .handle_modify(kopid.uat, msg, kopid.eventid) + .await; + to_axum_response(res) } -pub async fn delete(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let msg: DeleteRequest = req.body_json().await?; - let (eventid, hvalue) = req.new_eventid(); - let res = req.state().qe_w_ref.handle_delete(uat, msg, eventid).await; - to_tide_response(res, hvalue) +pub async fn v1_delete( + State(state): State, + Extension(kopid): Extension, + Json(msg): Json, +) -> Response { + let res = state + .qe_w_ref + .handle_delete(kopid.uat, msg, kopid.eventid) + .await; + to_axum_response(res) } -pub async fn search(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let msg: SearchRequest = req.body_json().await?; - let (eventid, hvalue) = req.new_eventid(); - let res = req.state().qe_r_ref.handle_search(uat, msg, eventid).await; - to_tide_response(res, hvalue) +pub async fn search( + State(state): State, + Extension(kopid): Extension, + Json(msg): Json, +) -> Response { + let res = state + .qe_r_ref + .handle_search(kopid.uat, msg, kopid.eventid) + .await; + to_axum_response(res) } -pub async fn whoami(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); +#[debug_handler] +pub async fn whoami( + State(state): State, + Extension(kopid): Extension, +) -> Response { // New event, feed current auth data from the token to it. - let res = req.state().qe_r_ref.handle_whoami(uat, eventid).await; - to_tide_response(res, hvalue) + let res = state.qe_r_ref.handle_whoami(kopid.uat, kopid.eventid).await; + to_axum_response(res) } -pub async fn whoami_uat(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); - let res = req.state().qe_r_ref.handle_whoami_uat(uat, eventid).await; - to_tide_response(res, hvalue) +pub async fn whoami_uat( + State(state): State, + Extension(kopid): Extension, + session: ReadableSession, +) -> impl IntoResponse { + let uat = match kopid.uat { + Some(val) => Some(val), + None => session.get("bearer"), + }; + let res = state.qe_r_ref.handle_whoami_uat(uat, kopid.eventid).await; + to_axum_response(res) } -pub async fn logout(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); - +pub async fn logout( + State(state): State, + mut msession: WritableSession, + Extension(kopid): Extension, +) -> impl IntoResponse { // Now lets nuke any cookies for the session. We do this before the handle_logout // so that if any errors occur, the cookies are still removed. - let msession = req.session_mut(); msession.remove("auth-session-id"); msession.remove("bearer"); - let res = req.state().qe_w_ref.handle_logout(uat, eventid).await; + let res = state.qe_w_ref.handle_logout(kopid.uat, kopid.eventid).await; - to_tide_response(res, hvalue) + to_axum_response(res) } -// =============== REST generics ======================== +// // =============== REST generics ======================== +#[instrument(level = "trace", skip(state, kopid))] pub async fn json_rest_event_get( - req: tide::Request, - filter: Filter, + state: ServerState, attrs: Option>, -) -> tide::Result { - let uat = req.get_current_uat(); - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + filter: Filter, + kopid: KOpId, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_internalsearch(uat, filter, attrs, eventid) + .handle_internalsearch(kopid.uat, filter, attrs, kopid.eventid) .await; - to_tide_response(res, hvalue) + + to_axum_response(res) } pub async fn json_rest_event_get_id( - req: tide::Request, + state: ServerState, + id: String, filter: Filter, attrs: Option>, -) -> tide::Result { - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - + kopid: KOpId, +) -> impl IntoResponse { let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str()))); - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_r_ref - .handle_internalsearch(uat, filter, attrs, eventid) + .handle_internalsearch(kopid.uat, filter, attrs, kopid.eventid) .await .map(|mut r| r.pop()); - to_tide_response(res, hvalue) + to_axum_response(res) } pub async fn json_rest_event_delete_id( - req: tide::Request, + state: ServerState, + id: String, filter: Filter, -) -> tide::Result { - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - + kopid: KOpId, +) -> impl IntoResponse { let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str()))); - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_w_ref - .handle_internaldelete(uat, filter, eventid) + .handle_internaldelete(kopid.uat, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } pub async fn json_rest_event_get_attr( - req: tide::Request, + state: ServerState, id: &str, + attr: String, filter: Filter, -) -> tide::Result { - let attr = req.get_url_param("attr")?; - let uat = req.get_current_uat(); + kopid: KOpId, +) -> impl IntoResponse { let filter = Filter::join_parts_and(filter, filter_all!(f_id(id))); - - let (eventid, hvalue) = req.new_eventid(); - let attrs = Some(vec![attr.clone()]); - - let res: Result, _> = req - .state() + let res: Result, _> = state .qe_r_ref - .handle_internalsearch(uat, filter, attrs, eventid) + .handle_internalsearch(kopid.uat, filter, attrs, kopid.eventid) .await .map(|mut event_result| event_result.pop().and_then(|mut e| e.attrs.remove(&attr))); - to_tide_response(res, hvalue) + to_axum_response(res) } pub async fn json_rest_event_get_id_attr( - req: tide::Request, + state: ServerState, + id: String, + attr: String, filter: Filter, -) -> tide::Result { - let id = req.get_url_param("id")?; - json_rest_event_get_attr(req, id.as_str(), filter).await + kopid: KOpId, +) -> impl IntoResponse { + json_rest_event_get_attr(state, id.as_str(), attr, filter, kopid).await } pub async fn json_rest_event_post( - mut req: tide::Request, + state: ServerState, classes: Vec, -) -> tide::Result { + obj: ProtoEntry, + kopid: KOpId, +) -> impl IntoResponse { debug_assert!(!classes.is_empty()); - let (eventid, hvalue) = req.new_eventid(); - // Read the json from the wire. - let uat = req.get_current_uat(); - let mut obj: ProtoEntry = req.body_json().await?; - obj.attrs.insert("class".to_string(), classes); - let msg = CreateRequest { entries: vec![obj] }; - let res = req.state().qe_w_ref.handle_create(uat, msg, eventid).await; - to_tide_response(res, hvalue) + let mut obj = obj; + obj.attrs.insert("class".to_string(), classes); + let msg = CreateRequest { + entries: vec![obj.to_owned()], + }; + + let res = state + .qe_w_ref + .handle_create(kopid.uat, msg, kopid.eventid) + .await; + to_axum_response(res) } pub async fn json_rest_event_post_id_attr( - mut req: tide::Request, + state: ServerState, + id: String, + attr: String, filter: Filter, -) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let attr = req.get_url_param("attr")?; - let values: Vec = req.body_json().await?; - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() + values: Vec, + kopid: KOpId, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_appendattribute(uat, uuid_or_name, attr, values, filter, eventid) + .handle_appendattribute(kopid.uat, id, attr, values, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } pub async fn json_rest_event_put_attr( - mut req: tide::Request, - uuid_or_name: String, + state: ServerState, + id: String, + attr: String, filter: Filter, -) -> tide::Result { - let uat = req.get_current_uat(); - let attr = req.get_url_param("attr")?; - let values: Vec = req.body_json().await?; - - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() + values: Vec, + kopid: KOpId, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_setattribute(uat, uuid_or_name, attr, values, filter, eventid) + .handle_setattribute(kopid.uat, id, attr, values, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } pub async fn json_rest_event_post_attr( - mut req: tide::Request, - uuid_or_name: String, + state: ServerState, + id: String, + attr: String, filter: Filter, -) -> tide::Result { - let uat = req.get_current_uat(); - let attr = req.get_url_param("attr")?; - let values: Vec = req.body_json().await?; - - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() + values: Vec, + kopid: KOpId, +) -> impl IntoResponse { + let uuid_or_name = id; + let res = state .qe_w_ref - .handle_appendattribute(uat, uuid_or_name, attr, values, filter, eventid) + .handle_appendattribute(kopid.uat, uuid_or_name, attr, values, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } pub async fn json_rest_event_put_id_attr( - req: tide::Request, + state: ServerState, + id: String, + attr: String, filter: Filter, -) -> tide::Result { - let uuid_or_name = req.get_url_param("id")?; - json_rest_event_put_attr(req, uuid_or_name, filter).await + values: Vec, + kopid: KOpId, +) -> impl IntoResponse { + json_rest_event_put_attr(state, id, attr, filter, values, kopid).await } pub async fn json_rest_event_delete_id_attr( - req: tide::Request, - filter: Filter, + state: ServerState, + id: String, attr: String, -) -> tide::Result { - let uuid_or_name = req.get_url_param("id")?; - json_rest_event_delete_attr(req, filter, uuid_or_name, attr).await + filter: Filter, + values: Option>, + kopid: KOpId, +) -> impl IntoResponse { + json_rest_event_delete_attr(state, id, attr, filter, values, kopid).await } pub async fn json_rest_event_delete_attr( - mut req: tide::Request, - filter: Filter, + state: ServerState, uuid_or_name: String, - // Separate for account_delete_id_radius attr: String, -) -> tide::Result { - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); - - // TODO #211: Attempt to get an option Vec here? - // It's probably better to focus on SCIM instead, it seems richer than this. - let body = req.take_body(); - let values: Vec = if body.is_empty().unwrap_or(true) { - vec![] - } else { - // Must now be a valid list. - body.into_json().await? + filter: Filter, + values: Option>, + kopid: KOpId, +) -> impl IntoResponse { + let values = match values { + Some(val) => val, + None => vec![], }; if values.is_empty() { - let res = req - .state() + let res = state .qe_w_ref - .handle_purgeattribute(uat, uuid_or_name, attr, filter, eventid) + .handle_purgeattribute(kopid.uat, uuid_or_name, attr, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } else { - let res = req - .state() + let res = state .qe_w_ref - .handle_removeattributevalues(uat, uuid_or_name, attr, values, filter, eventid) + .handle_removeattributevalues( + kopid.uat, + uuid_or_name, + attr, + values, + filter, + kopid.eventid, + ) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } } -// Okay, so a put normally needs -// * filter of what we are working on (id + class) -// * a Map> that we turn into a modlist. -// -// OR -// * filter of what we are working on (id + class) -// * a Vec that we are changing -// * the attr name (as a param to this in path) -// -// json_rest_event_put_id(path, req, state +// // Okay, so a put normally needs +// // * filter of what we are working on (id + class) +// // * a Map> that we turn into a modlist. +// // +// // OR +// // * filter of what we are working on (id + class) +// // * a Vec that we are changing +// // * the attr name (as a param to this in path) +// // +// // json_rest_event_put_id(path, req, state -pub async fn schema_get(req: tide::Request) -> tide::Result { +pub async fn schema_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { // NOTE: This is filter_all, because from_internal_message will still do the alterations // needed to make it safe. This is needed because there may be aci's that block access // to the recycle/ts types in the filter, and we need the aci to only eval on this @@ -318,828 +353,976 @@ pub async fn schema_get(req: tide::Request) -> tide::Result { f_eq("class", PartialValue::new_class("attributetype")), f_eq("class", PartialValue::new_class("classtype")) ])); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } -pub async fn schema_attributetype_get(req: tide::Request) -> tide::Result { +pub async fn schema_attributetype_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("attributetype"))); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } -pub async fn schema_attributetype_get_id(req: tide::Request) -> tide::Result { - // These can't use get_id because they attribute name and class name aren't ... well name. - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - +pub async fn schema_attributetype_get_id( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + // These can't use get_id because the attribute name and class name aren't ... well name. let filter = filter_all!(f_and!([ f_eq("class", PartialValue::new_class("attributetype")), f_eq("attributename", PartialValue::new_iutf8(id.as_str())) ])); - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_r_ref - .handle_internalsearch(uat, filter, None, eventid) + .handle_internalsearch(kopid.uat, filter, None, kopid.eventid) .await .map(|mut r| r.pop()); - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn schema_classtype_get(req: tide::Request) -> tide::Result { +pub async fn schema_classtype_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("classtype"))); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } -pub async fn schema_classtype_get_id(req: tide::Request) -> tide::Result { +pub async fn schema_classtype_get_id( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { // These can't use get_id because they attribute name and class name aren't ... well name. - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - let filter = filter_all!(f_and!([ f_eq("class", PartialValue::new_class("classtype")), f_eq("classname", PartialValue::new_iutf8(id.as_str())) ])); - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_r_ref - .handle_internalsearch(uat, filter, None, eventid) + .handle_internalsearch(kopid.uat, filter, None, kopid.eventid) .await .map(|mut r| r.pop()); - to_tide_response(res, hvalue) + to_axum_response(res) } -// == person == - -pub async fn person_get(req: tide::Request) -> tide::Result { +// // == person == +pub async fn person_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("person"))); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } // expects the following fields in the attrs field of the req: [name, displayname] -pub async fn person_post(req: tide::Request) -> tide::Result { +#[debug_handler] +pub async fn person_post( + State(state): State, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { let classes = vec![ "person".to_string(), "account".to_string(), "object".to_string(), ]; - json_rest_event_post(req, classes).await + json_rest_event_post(state, classes, obj, kopid).await } -pub async fn person_id_get(req: tide::Request) -> tide::Result { +pub async fn person_id_get( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("person"))); - json_rest_event_get_id(req, filter, None).await + json_rest_event_get_id(state, id, filter, None, kopid).await } -pub async fn person_account_id_delete(req: tide::Request) -> tide::Result { +pub async fn person_account_id_delete( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("person"))); - json_rest_event_delete_id(req, filter).await + json_rest_event_delete_id(state, id, filter, kopid).await } -// == account == +// // == account == -pub async fn service_account_get(req: tide::Request) -> tide::Result { +pub async fn service_account_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("service_account"))); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } -pub async fn service_account_post(req: tide::Request) -> tide::Result { +pub async fn service_account_post( + State(state): State, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { let classes = vec![ "service_account".to_string(), "account".to_string(), "object".to_string(), ]; - json_rest_event_post(req, classes).await + json_rest_event_post(state, classes, obj, kopid).await } -pub async fn service_account_id_get(req: tide::Request) -> tide::Result { +pub async fn service_account_id_get( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("service_account"))); - json_rest_event_get_id(req, filter, None).await + json_rest_event_get_id(state, id, filter, None, kopid).await } -pub async fn service_account_id_delete(req: tide::Request) -> tide::Result { - let filter = filter_all!(f_eq("class", PartialValue::new_class("service_account"))); - json_rest_event_delete_id(req, filter).await +pub async fn service_account_id_delete( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + let filter = filter_all!(f_eq("class", PartialValue::new_class("service_accont"))); + json_rest_event_delete_id(state, id, filter, kopid).await } -pub async fn service_account_credential_generate(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn service_account_credential_generate( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_service_account_credential_generate(uat, uuid_or_name, eventid) + .handle_service_account_credential_generate(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -// Due to how the migrations work in 6 -> 7, we can accidentally -// mark "accounts" as service accounts when they are persons. This -// allows migrating them to the person type due to it's similarities. -// -// In the future this will be REMOVED! -pub async fn service_account_into_person(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +// // Due to how the migrations work in 6 -> 7, we can accidentally +// // mark "accounts" as service accounts when they are persons. This +// // allows migrating them to the person type due to it's similarities. +// // +// // In the future this will be REMOVED! +pub async fn service_account_into_person( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_service_account_into_person(uat, uuid_or_name, eventid) + .handle_service_account_into_person(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -// Api Token -pub async fn service_account_api_token_get(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +// // Api Token +pub async fn service_account_api_token_get( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_service_account_api_token_get(uat, uuid_or_name, eventid) + .handle_service_account_api_token_get(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn service_account_api_token_post(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let ApiTokenGenerate { - label, - expiry, - read_write, - } = req.body_json().await?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn service_account_api_token_post( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, + Json(obj): Json, // TODO work out if this limits the fields? +) -> impl IntoResponse { + let res = state .qe_w_ref .handle_service_account_api_token_generate( - uat, - uuid_or_name, - label, - expiry, - read_write, - eventid, + kopid.uat, + id, + obj.label, + obj.expiry, + obj.read_write, + kopid.eventid, ) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn service_account_api_token_delete(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let token_id = req.get_url_param_uuid("token_id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn service_account_api_token_delete( + State(state): State, + Path((id, token_id)): Path<(String, Uuid)>, + Extension(kopid): Extension, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_service_account_api_token_destroy(uat, uuid_or_name, token_id, eventid) + .handle_service_account_api_token_destroy(kopid.uat, id, token_id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -// Account stuff -pub async fn account_id_get_attr(req: tide::Request) -> tide::Result { +// // Account stuff +// TODO: shouldn't this be service_account? +pub async fn account_id_get_attr( + State(state): State, + Path((id, attr)): Path<(String, String)>, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); - json_rest_event_get_id_attr(req, filter).await + json_rest_event_get_attr(state, id.as_str(), attr, filter, kopid).await } -pub async fn account_id_post_attr(req: tide::Request) -> tide::Result { +pub async fn account_id_post_attr( + State(state): State, + Path((id, attr)): Path<(String, String)>, + Extension(kopid): Extension, + Json(values): Json>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); - json_rest_event_post_id_attr(req, filter).await + json_rest_event_post_id_attr(state, id, attr, filter, values, kopid).await } -pub async fn account_id_delete_attr(req: tide::Request) -> tide::Result { +pub async fn account_id_delete_attr( + State(state): State, + Path((id, attr)): Path<(String, String)>, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); - let attr = req.get_url_param("attr")?; - json_rest_event_delete_id_attr(req, filter, attr).await + json_rest_event_delete_id_attr(state, id, attr, filter, None, kopid).await } -pub async fn account_id_put_attr(req: tide::Request) -> tide::Result { +pub async fn account_id_put_attr( + State(state): State, + Path((id, attr)): Path<(String, String)>, + Extension(kopid): Extension, + Json(values): Json>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); - json_rest_event_put_id_attr(req, filter).await + json_rest_event_put_attr(state, id, attr, filter, values, kopid).await } -pub async fn account_id_patch(mut req: tide::Request) -> tide::Result { +pub async fn account_id_patch( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, + Json(obj): Json, +) -> impl IntoResponse { // Update a value / attrs - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - - let obj: ProtoEntry = req.body_json().await?; let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str()))); - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_w_ref - .handle_internalpatch(uat, filter, obj, eventid) + .handle_internalpatch(kopid.uat, filter, obj, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_get_id_credential_update(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn account_get_id_credential_update( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_idmcredentialupdate(uat, uuid_or_name, eventid) + .handle_idmcredentialupdate(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_get_id_credential_update_intent(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let ttl = req - .param("ttl") - .ok() - .and_then(|s| { - u64::from_str(s) - .map_err(|_e| { - error!("Invalid TTL integer, ignoring."); - }) - .ok() - }) - .map(Duration::from_secs); - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +#[instrument(level = "trace", skip(state, kopid))] +pub async fn account_get_id_credential_update_intent_ttl( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, + Query(ttl): Query, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_idmcredentialupdateintent(uat, uuid_or_name, ttl, eventid) + .handle_idmcredentialupdateintent( + kopid.uat, + id, + Some(Duration::from_secs(ttl)), + kopid.eventid, + ) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_get_id_user_auth_token(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; +#[instrument(level = "trace", skip(state, kopid))] +pub async fn account_get_id_credential_update_intent( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { + let res = state + .qe_w_ref + .handle_idmcredentialupdateintent(kopid.uat, id, None, kopid.eventid) + .await; + // panic!("res: {:?}", res); + to_axum_response(res) +} - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn account_get_id_user_auth_token( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_account_user_auth_token_get(uat, uuid_or_name, eventid) + .handle_account_user_auth_token_get(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_user_auth_token_delete(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let token_id = req.get_url_param_uuid("token_id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn account_user_auth_token_delete( + State(state): State, + Path((id, token_id)): Path<(String, Uuid)>, + Extension(kopid): Extension, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_account_user_auth_token_destroy(uat, uuid_or_name, token_id, eventid) + .handle_account_user_auth_token_destroy(kopid.uat, id, token_id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn credential_update_exchange_intent(mut req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - let intent_token: CUIntentToken = req.body_json().await?; - - let res = req - .state() +pub async fn credential_update_exchange_intent( + State(state): State, + Extension(kopid): Extension, + Json(intent_token): Json, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_idmcredentialexchangeintent(intent_token, eventid) + .handle_idmcredentialexchangeintent(intent_token, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn credential_update_status(mut req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - let session_token: CUSessionToken = req.body_json().await?; - - let res = req - .state() +pub async fn credential_update_status( + State(state): State, + Extension(kopid): Extension, + Json(session_token): Json, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_idmcredentialupdatestatus(session_token, eventid) + .handle_idmcredentialupdatestatus(session_token, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn credential_update_update(mut req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - let (scr, session_token): (CURequest, CUSessionToken) = req.body_json().await?; - - let res = req - .state() +// #[derive(Deserialize, Debug, Clone)] +// struct CUBody { +// pub session_token: CUSessionToken, +// pub scr: CURequest, +// } +#[instrument(level = "debug", skip(state, kopid))] +pub async fn credential_update_update( + State(state): State, + Extension(kopid): Extension, + Json(cubody): Json>, +) -> impl IntoResponse { + let scr: CURequest = match serde_json::from_value(cubody[0].clone()) { + Ok(val) => val, + Err(err) => { + error!("Failed to deserialize CURequest: {:?}", err); + #[allow(clippy::unwrap_used)] + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .unwrap(); + } + }; + let session_token = match serde_json::from_value(cubody[1].clone()) { + Ok(val) => val, + Err(err) => { + error!("Failed to deserialize session token: {:?}", err); + #[allow(clippy::unwrap_used)] + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .unwrap(); + } + }; + debug!("session_token: {:?}", session_token); + debug!("scr: {:?}", scr); + let res = state .qe_r_ref - .handle_idmcredentialupdate(session_token, scr, eventid) + .handle_idmcredentialupdate(session_token, scr, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn credential_update_commit(mut req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - let session_token: CUSessionToken = req.body_json().await?; - - let res = req - .state() +pub async fn credential_update_commit( + State(state): State, + Extension(kopid): Extension, + Json(session_token): Json, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_idmcredentialupdatecommit(session_token, eventid) + .handle_idmcredentialupdatecommit(session_token, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn credential_update_cancel(mut req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - let session_token: CUSessionToken = req.body_json().await?; - - let res = req - .state() +pub async fn credential_update_cancel( + State(state): State, + Extension(kopid): Extension, + Json(session_token): Json, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_idmcredentialupdatecancel(session_token, eventid) + .handle_idmcredentialupdatecancel(session_token, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_get_id_credential_status(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn account_get_id_credential_status( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_idmcredentialstatus(uat, uuid_or_name, eventid) + .handle_idmcredentialstatus(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -// Return a vec of str -pub async fn account_get_id_ssh_pubkeys(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +// // Return a vec of str +pub async fn account_get_id_ssh_pubkeys( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_internalsshkeyread(uat, uuid_or_name, eventid) + .handle_internalsshkeyread(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_post_id_ssh_pubkey(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let (tag, key): (String, String) = req.body_json().await?; +pub async fn account_post_id_ssh_pubkey( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, + Json((tag, key)): Json<(String, String)>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); - - let (eventid, hvalue) = req.new_eventid(); // Add a msg here - let res = req - .state() + let res = state .qe_w_ref - .handle_sshkeycreate(uat, uuid_or_name, tag, key, filter, eventid) + .handle_sshkeycreate(kopid.uat, id, tag, key, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_get_id_ssh_pubkey_tag(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let tag = req.get_url_param("tag")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn account_get_id_ssh_pubkey_tag( + State(state): State, + Extension(kopid): Extension, + Path((id, tag)): Path<(String, String)>, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_internalsshkeytagread(uat, uuid_or_name, tag, eventid) + .handle_internalsshkeytagread(kopid.uat, id, tag, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_delete_id_ssh_pubkey_tag(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let tag = req.get_url_param("tag")?; +pub async fn account_delete_id_ssh_pubkey_tag( + State(state): State, + Extension(kopid): Extension, + Path((id, tag)): Path<(String, String)>, +) -> impl IntoResponse { let attr = "ssh_publickey".to_string(); let values = vec![tag]; let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_w_ref - .handle_removeattributevalues(uat, uuid_or_name, attr, values, filter, eventid) + .handle_removeattributevalues(kopid.uat, id, attr, values, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -// Get and return a single str -pub async fn account_get_id_radius(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +// // Get and return a single str +pub async fn account_get_id_radius( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_internalradiusread(uat, uuid_or_name, eventid) + .handle_internalradiusread(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_post_id_radius_regenerate(req: tide::Request) -> tide::Result { +pub async fn account_post_id_radius_regenerate( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { // Need to to send the regen msg - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_w_ref - .handle_regenerateradius(uat, uuid_or_name, eventid) + .handle_regenerateradius(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_delete_id_radius(req: tide::Request) -> tide::Result { +pub async fn account_delete_id_radius( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { let attr = "radius_secret".to_string(); let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); - json_rest_event_delete_id_attr(req, filter, attr).await + json_rest_event_delete_id_attr(state, id, attr, filter, None, kopid).await } -pub async fn account_get_id_radius_token(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn account_get_id_radius_token( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_internalradiustokenread(uat, uuid_or_name, eventid) + .handle_internalradiustokenread(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + let mut res = to_axum_response(res); + debug!("Response: {:?}", res); + let cache_header = CacheControl::new() + .with_max_age(Duration::from_secs(300)) + .with_private(); + res.headers_mut().typed_insert(cache_header); + res } -pub async fn account_post_id_unix(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let obj: AccountUnixExtend = req.body_json().await?; - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() +pub async fn account_post_id_unix( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_idmaccountunixextend(uat, uuid_or_name, obj, eventid) + .handle_idmaccountunixextend(kopid.uat, id, obj, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_get_id_unix_token(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn account_get_id_unix_token( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_internalunixusertokenread(uat, uuid_or_name, eventid) + .handle_internalunixusertokenread(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_post_id_unix_auth(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let obj: SingleStringRequest = req.body_json().await?; - let cred = obj.value; - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() +pub async fn account_post_id_unix_auth( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, + Json(obj): Json, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_idmaccountunixauth(uat, uuid_or_name, cred, eventid) + .handle_idmaccountunixauth(kopid.uat, id, obj.value, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_put_id_unix_credential(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let obj: SingleStringRequest = req.body_json().await?; - let cred = obj.value; - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() +pub async fn account_put_id_unix_credential( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, + Json(obj): Json, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_idmaccountunixsetcred(uat, uuid_or_name, cred, eventid) + .handle_idmaccountunixsetcred(kopid.uat, id, obj.value, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn account_delete_id_unix_credential(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let attr = "unix_password".to_string(); +pub async fn account_delete_id_unix_credential( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("posixaccount"))); - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_w_ref - .handle_purgeattribute(uat, uuid_or_name, attr, filter, eventid) + .handle_purgeattribute( + kopid.uat, + id, + "unix_password".to_string(), + filter, + kopid.eventid, + ) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn group_get(req: tide::Request) -> tide::Result { +pub async fn group_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("group"))); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } -pub async fn group_post(req: tide::Request) -> tide::Result { +pub async fn group_post( + State(state): State, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { let classes = vec!["group".to_string(), "object".to_string()]; - json_rest_event_post(req, classes).await + json_rest_event_post(state, classes, obj, kopid).await } -pub async fn group_id_get(req: tide::Request) -> tide::Result { +pub async fn group_id_get( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("group"))); - json_rest_event_get_id(req, filter, None).await + json_rest_event_get_id(state, id, filter, None, kopid).await } -pub async fn group_id_get_attr(req: tide::Request) -> tide::Result { +pub async fn group_id_get_attr( + State(state): State, + Path((id, attr)): Path<(String, String)>, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("group"))); - json_rest_event_get_id_attr(req, filter).await + json_rest_event_get_id_attr(state, id, attr, filter, kopid).await } -pub async fn group_id_post_attr(req: tide::Request) -> tide::Result { +pub async fn group_id_post_attr( + Path((id, attr)): Path<(String, String)>, + State(state): State, + Extension(kopid): Extension, + Json(values): Json>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("group"))); - json_rest_event_post_id_attr(req, filter).await + json_rest_event_post_id_attr(state, id, attr, filter, values, kopid).await } -pub async fn group_id_delete_attr(req: tide::Request) -> tide::Result { +pub async fn group_id_delete_attr( + Path((id, attr)): Path<(String, String)>, + State(state): State, + Extension(kopid): Extension, + values: Option>>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("group"))); - let attr = req.get_url_param("attr")?; - json_rest_event_delete_id_attr(req, filter, attr).await + let values = values.map(|v| v.0); + json_rest_event_delete_id_attr(state, id, attr, filter, values, kopid).await } -pub async fn group_id_put_attr(req: tide::Request) -> tide::Result { +pub async fn group_id_put_attr( + Path((id, attr)): Path<(String, String)>, + State(state): State, + Extension(kopid): Extension, + Json(values): Json>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("group"))); - json_rest_event_put_id_attr(req, filter).await + json_rest_event_put_id_attr(state, id, attr, filter, values, kopid).await } - -pub async fn group_id_delete(req: tide::Request) -> tide::Result { +pub async fn group_id_delete( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("group"))); - json_rest_event_delete_id(req, filter).await + json_rest_event_delete_id(state, id, filter, kopid).await } -pub async fn group_post_id_unix(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - let obj: GroupUnixExtend = req.body_json().await?; - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() +pub async fn group_post_id_unix( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_idmgroupunixextend(uat, uuid_or_name, obj, eventid) + .handle_idmgroupunixextend(kopid.uat, id, obj, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn group_get_id_unix_token(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn group_get_id_unix_token( + State(state): State, + Extension(kopid): Extension, + Path(id): Path, +) -> impl IntoResponse { + let res = state .qe_r_ref - .handle_internalunixgrouptokenread(uat, uuid_or_name, eventid) + .handle_internalunixgrouptokenread(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn domain_get(req: tide::Request) -> tide::Result { +pub async fn domain_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("uuid", PartialValue::Uuid(UUID_DOMAIN_INFO))); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } -pub async fn domain_get_attr(req: tide::Request) -> tide::Result { +pub async fn domain_get_attr( + State(state): State, + Extension(kopid): Extension, + Path(attr): Path, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info"))); - json_rest_event_get_attr(req, STR_UUID_DOMAIN_INFO, filter).await + json_rest_event_get_attr(state, STR_UUID_DOMAIN_INFO, attr, filter, kopid).await } -pub async fn domain_put_attr(req: tide::Request) -> tide::Result { +pub async fn domain_put_attr( + State(state): State, + Extension(kopid): Extension, + Path(attr): Path, + Json(values): Json>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info"))); - json_rest_event_put_attr(req, STR_UUID_DOMAIN_INFO.to_string(), filter).await + json_rest_event_put_attr( + state, + STR_UUID_DOMAIN_INFO.to_string(), + attr, + filter, + values, + kopid, + ) + .await } -pub async fn domain_delete_attr(req: tide::Request) -> tide::Result { +pub async fn domain_delete_attr( + State(state): State, + Path(attr): Path, + Extension(kopid): Extension, + Json(values): Json>>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info"))); - let attr = req.get_url_param("attr")?; - json_rest_event_delete_attr(req, filter, STR_UUID_DOMAIN_INFO.to_string(), attr).await + json_rest_event_delete_attr( + state, + STR_UUID_DOMAIN_INFO.to_string(), + attr, + filter, + values, + kopid, + ) + .await } -pub async fn system_get(req: tide::Request) -> tide::Result { +pub async fn system_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("uuid", PartialValue::Uuid(UUID_SYSTEM_CONFIG))); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } -pub async fn system_get_attr(req: tide::Request) -> tide::Result { +pub async fn system_get_attr( + State(state): State, + Path(attr): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("system_config"))); - json_rest_event_get_attr(req, STR_UUID_SYSTEM_CONFIG, filter).await + json_rest_event_get_attr(state, STR_UUID_SYSTEM_CONFIG, attr, filter, kopid).await } -pub async fn system_post_attr(req: tide::Request) -> tide::Result { +pub async fn system_post_attr( + State(state): State, + Path(attr): Path, + Extension(kopid): Extension, + Json(values): Json>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("system_config"))); - json_rest_event_post_attr(req, STR_UUID_SYSTEM_CONFIG.to_string(), filter).await + json_rest_event_post_attr( + state, + STR_UUID_SYSTEM_CONFIG.to_string(), + attr, + filter, + values, + kopid, + ) + .await } -pub async fn system_delete_attr(req: tide::Request) -> tide::Result { +pub async fn system_delete_attr( + State(state): State, + Path(attr): Path, + Extension(kopid): Extension, + Json(values): Json>>, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("system_config"))); - let attr = req.get_url_param("attr")?; - json_rest_event_delete_attr(req, filter, STR_UUID_SYSTEM_CONFIG.to_string(), attr).await + json_rest_event_delete_attr( + state, + STR_UUID_SYSTEM_CONFIG.to_string(), + attr, + filter, + values, + kopid, + ) + .await } -pub async fn recycle_bin_get(req: tide::Request) -> tide::Result { +pub async fn recycle_bin_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_pres("class")); - let uat = req.get_current_uat(); let attrs = None; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_r_ref - .handle_internalsearchrecycled(uat, filter, attrs, eventid) + .handle_internalsearchrecycled(kopid.uat, filter, attrs, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn recycle_bin_id_get(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; +pub async fn recycle_bin_id_get( + State(state): State, + + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_id(id.as_str())); let attrs = None; - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_r_ref - .handle_internalsearchrecycled(uat, filter, attrs, eventid) + .handle_internalsearchrecycled(kopid.uat, filter, attrs, kopid.eventid) .await .map(|mut r| r.pop()); - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn recycle_bin_revive_id_post(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; +pub async fn recycle_bin_revive_id_post( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_id(id.as_str())); - - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() + let res = state .qe_w_ref - .handle_reviverecycled(uat, filter, eventid) + .handle_reviverecycled(kopid.uat, filter, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn applinks_get(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); - let res = req - .state() +pub async fn applinks_get( + State(state): State, + Extension(kopid): Extension, + session: ReadableSession, +) -> impl IntoResponse { + let uat = match kopid.uat { + Some(val) => Some(val), + None => session.get("bearer"), + }; + let res = state .qe_r_ref - .handle_list_applinks(uat, eventid) + .handle_list_applinks(uat, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn do_routemap(req: tide::Request) -> tide::Result { - let mut res = tide::Response::new(200); +// TODO: routemap things +// pub async fn do_routemap(State(state): State) -> impl IntoResponse { +// Json(state.do_map()) +// } - res.set_body(req.state().do_map()); - Ok(res) -} +pub async fn reauth( + State(state): State, + ConnectInfo(addr): ConnectInfo, // TODO: test x-ff-headers + Extension(kopid): Extension, + session: WritableSession, + Json(obj): Json, +) -> impl IntoResponse { + // TODO: xff things check that we can get the remote IP address first, since this doesn't touch the backend at all + // let ip_addr = req.get_remote_addr().ok_or_else(|| { + // error!("Unable to get remote addr for auth event, refusing to proceed"); + // tide::Error::from_str( + // tide::StatusCode::InternalServerError, + // "unable to validate peer address", + // ) + // })?; -pub async fn do_nothing(_req: tide::Request) -> tide::Result { - let mut res = tide::Response::new(200); - res.set_body("did nothing"); - Ok(res) -} - -pub async fn reauth(mut req: tide::Request) -> tide::Result { - // check that we can get the remote IP address first, since this doesn't touch the backend at all - let ip_addr = req.get_remote_addr().ok_or_else(|| { - error!("Unable to get remote addr for auth event, refusing to proceed"); - tide::Error::from_str( - tide::StatusCode::InternalServerError, - "unable to validate peer address", - ) - })?; - - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); - - let obj: AuthIssueSession = req.body_json().await.map_err(|e| { - debug!("Failed get body JSON? {:?}", e); - e - })?; - - let inter = req - .state() - // This may change in the future ... + // This may change in the future ... + let inter = state .qe_r_ref - .handle_reauth(uat, obj, eventid, ip_addr) + .handle_reauth(kopid.uat, obj, kopid.eventid, addr.ip()) .await; - - auth_session_state_management(req, inter, hvalue) + debug!("REAuth result: {:?}", inter); + auth_session_state_management(state, inter, session) } -pub async fn auth(mut req: tide::Request) -> tide::Result { +pub async fn auth( + State(state): State, + session: WritableSession, + headers: HeaderMap, + Extension(kopid): Extension, + ConnectInfo(addr): ConnectInfo, + Json(obj): Json, +) -> impl IntoResponse { + // TODO: check this trusts the x-ff-header + let ip_addr = addr.ip(); // check that we can get the remote IP address first, since this doesn't touch the backend at all - let ip_addr = req.get_remote_addr().ok_or_else(|| { - error!("Unable to get remote addr for auth event, refusing to proceed"); - tide::Error::from_str( - tide::StatusCode::InternalServerError, - "unable to validate peer address", - ) - })?; + // let ip_addr = req.get_remote_addr().ok_or_else(|| { + // error!("Unable to get remote addr for auth event, refusing to proceed"); + // tide::Error::from_str( + // tide::StatusCode::InternalServerError, + // "unable to validate peer address", + // ) + // })?; + // First, deal with some state management. // Do anything here first that's needed like getting the session details // out of the req cookie. - let (eventid, hvalue) = req.new_eventid(); - - let maybe_sessionid: Option = req.get_current_auth_session_id(); - - let obj: AuthRequest = req.body_json().await.map_err(|e| { - debug!("Failed get body JSON? {:?}", e); - e - })?; + // TODO + let maybe_sessionid = state.get_current_auth_session_id(&headers, &session); + debug!("Session ID: {:?}", maybe_sessionid); // We probably need to know if we allocate the cookie, that this is a // new session, and in that case, anything *except* authrequest init is // invalid. - let inter = req - .state() - // This may change in the future ... + let inter = state // This may change in the future ... .qe_r_ref - .handle_auth(maybe_sessionid, obj, eventid, ip_addr) + .handle_auth(maybe_sessionid, obj, kopid.eventid, ip_addr) .await; - auth_session_state_management(req, inter, hvalue) + debug!("Auth result: {:?}", inter); + + auth_session_state_management(state, inter, session) } +#[instrument(skip(state))] fn auth_session_state_management( - mut req: tide::Request, + state: ServerState, inter: Result, - hvalue: String, -) -> tide::Result { + mut msession: WritableSession, +) -> impl IntoResponse { let mut auth_session_id_tok = None; let res: Result = match inter { - Ok(AuthResult { state, sessionid }) => { + Ok(AuthResult { + state: auth_state, + sessionid, + }) => { // Do some response/state management. - match state { + match auth_state { AuthState::Choose(allowed) => { - debug!("🧩 -> AuthState::Choose"); - let msession = req.session_mut(); + debug!("🧩 -> AuthState::Choose"); // TODO: this should be ... less work // Ensure the auth-session-id is set msession.remove("auth-session-id"); @@ -1150,8 +1333,7 @@ fn auth_session_state_management( OperationError::InvalidSessionState }) .and_then(|_| { - let kref = &req.state().jws_signer; - + let kref = &state.jws_signer; let jws = Jws::new(SessionId { sessionid }); // Get the header token ready. jws.sign(kref) @@ -1167,7 +1349,7 @@ fn auth_session_state_management( } AuthState::Continue(allowed) => { debug!("🧩 -> AuthState::Continue"); - let msession = req.session_mut(); + // Ensure the auth-session-id is set msession.remove("auth-session-id"); trace!(?sessionid, "🔥 🔥 "); @@ -1178,7 +1360,7 @@ fn auth_session_state_management( OperationError::InvalidSessionState }) .and_then(|_| { - let kref = &req.state().jws_signer; + let kref = &state.jws_signer; // Get the header token ready. let jws = Jws::new(SessionId { sessionid }); jws.sign(kref) @@ -1195,7 +1377,7 @@ fn auth_session_state_management( AuthState::Success(token, issue) => { debug!("🧩 -> AuthState::Success"); // Remove the auth-session-id - let msession = req.session_mut(); + msession.remove("auth-session-id"); // Create a session cookie? msession.remove("bearer"); @@ -1210,7 +1392,6 @@ fn auth_session_state_management( } AuthState::Denied(reason) => { debug!("🧩 -> AuthState::Denied"); - let msession = req.session_mut(); // Remove the auth-session-id msession.remove("auth-session-id"); Ok(ProtoAuthState::Denied(reason)) @@ -1221,35 +1402,315 @@ fn auth_session_state_management( Err(e) => Err(e), }; - to_tide_response(res, hvalue).map(|mut res| { - // if the sessionid was injected into our cookie, set it in the - // header too. - if let Some(tok) = auth_session_id_tok { - res.insert_header("X-KANIDM-AUTH-SESSION-ID", tok); + let mut res = to_axum_response(res); + + // if the sessionid was injected into our cookie, set it in the header too. + match auth_session_id_tok { + Some(tok) => { + #[allow(clippy::unwrap_used)] + res.headers_mut().insert( + "X-KANIDM-AUTH-SESSION-ID", + HeaderValue::from_str(&tok).unwrap(), + ); + res } - res - }) + None => res, + } } -pub async fn auth_valid(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let (eventid, hvalue) = req.new_eventid(); - let res = req.state().qe_r_ref.handle_auth_valid(uat, eventid).await; - to_tide_response(res, hvalue) +pub async fn auth_valid( + State(state): State, + Extension(kopid): Extension, + session: ReadableSession, +) -> impl IntoResponse { + let uat = match kopid.uat { + Some(val) => Some(val), + None => session.get("bearer"), + }; + let res = state.qe_r_ref.handle_auth_valid(uat, kopid.eventid).await; + to_axum_response(res) } -// == Status - -pub async fn status(req: tide::Request) -> tide::Result { - // We ignore the body in this req - let (eventid, hvalue) = req.new_eventid(); - let r = req - .state() - .status_ref - .handle_request(StatusRequestEvent { eventid }) - .await; - let mut res = tide::Response::new(tide::StatusCode::Ok); - res.insert_header("X-KANIDM-OPID", hvalue); - res.set_body(tide::Body::from_json(&r)?); - Ok(res) +#[instrument(skip(state))] +pub fn router(state: ServerState) -> Router { + Router::new() + .route("/oauth2", get(super::oauth2::oauth2_get)) + .route( + "/oauth2/:rs_name", + get(super::oauth2::oauth2_id_get) + .patch(super::oauth2::oauth2_id_patch) + .delete(super::oauth2::oauth2_id_delete), + ) + .route( + "/oauth2/:rs_name/_basic_secret", + get(super::oauth2::oauth2_id_get_basic_secret), + ) + .route("/oauth2/_basic", post(super::oauth2::oauth2_basic_post)) + .route( + "/oauth2/:rs_name/_scopemap/:group", + post(super::oauth2::oauth2_id_scopemap_post) + .delete(super::oauth2::oauth2_id_scopemap_delete), + ) + .route( + "/oauth2/:rs_name/_sup_scopemap/:group", + post(super::oauth2::oauth2_id_sup_scopemap_post) + .delete(super::oauth2::oauth2_id_sup_scopemap_delete), + ) + .route("/raw/create", post(create)) + .route("/raw/modify", post(v1_modify)) + .route("/raw/delete", post(v1_delete)) + .route("/raw/search", post(search)) + .route("/schema", get(schema_get)) + .route( + "/schema/attributetype", + get(schema_attributetype_get), // post(|| async { "TODO" }) + ) + .route( + "/schema/attributetype/:id", + get(schema_attributetype_get_id), + ) + // .route("/schema/attributetype/:id", put(|| async { "TODO" }).patch(|| async { "TODO" })) + .route( + "/schema/classtype", + get(schema_classtype_get), // .post(|| async { "TODO" }) + ) + .route( + "/schema/classtype/:id", + get(schema_classtype_get_id) + .put(|| async { "TODO" }) + .patch(|| async { "TODO" }), + ) + .route("/self", get(whoami)) + .route("/self/_uat", get(whoami_uat)) + .route("/self/_attr/:attr", get(|| async { "TODO" })) + .route("/self/_credential", get(|| async { "TODO" })) + .route("/self/_credential/:cid/_lock", get(|| async { "TODO" })) + .route( + "/self/_radius", + get(|| async { "TODO" }) + .delete(|| async { "TODO" }) + .post(|| async { "TODO" }), + ) + .route("/self/_radius/_config", post(|| async { "TODO" })) + .route("/self/_radius/_config/:token", get(|| async { "TODO" })) + .route( + "/self/_radius/_config/:token/apple", + get(|| async { "TODO" }), + ) + // Applinks are the list of apps this account can access. + .route("/self/_applinks", get(applinks_get)) + // Person routes + .route("/person", get(person_get)) + .route("/person", post(person_post)) + .route( + "/person/:id", + get(person_id_get) + .patch(account_id_patch) + .delete(person_account_id_delete), + ) + .route( + "/person/:id/_attr/:attr", + get(account_id_get_attr) + .put(account_id_put_attr) + .post(account_id_post_attr) + .delete(account_id_delete_attr), + ) + .route("/person/:id/_lock", get(|| async { "TODO" })) + .route("/person/:id/_credential", get(|| async { "TODO" })) + .route( + "/person/:id/_credential/_status", + get(account_get_id_credential_status), + ) + .route( + "/person/:id/_credential/:cid/_lock", + get(|| async { "TODO" }), + ) + .route( + "/person/:id/_credential/_update", + get(account_get_id_credential_update), + ) + .route( + "/person/:id/_credential/_update_intent/:ttl", + get(account_get_id_credential_update_intent_ttl), + ) + .route( + "/person/:id/_credential/_update_intent", + get(account_get_id_credential_update_intent), + ) + .route( + "/person/:id/_ssh_pubkeys", + get(account_get_id_ssh_pubkeys).post(account_post_id_ssh_pubkey), + ) + .route( + "/person/:id/_ssh_pubkeys/:tag", + get(account_get_id_ssh_pubkey_tag).delete(account_delete_id_ssh_pubkey_tag), + ) + .route( + "/person/:id/_radius", + get(account_get_id_radius) + .post(account_post_id_radius_regenerate) + .delete(account_delete_id_radius), + ) + .route( + "/person/:id/_radius/_token", + get(account_get_id_radius_token), + ) // TODO: make this cacheable + .route("/person/:id/_unix", post(account_post_id_unix)) + .route( + "/person/:id/_unix/_credential", + put(account_put_id_unix_credential).delete(account_delete_id_unix_credential), + ) + // Service accounts + .route( + "/service_account", + get(service_account_get).post(service_account_post), + ) + .route( + "/service_account/", + get(service_account_get).post(service_account_post), + ) + .route( + "/service_account/:id", + get(service_account_id_get).delete(service_account_id_delete), + ) + .route( + "/service_account/:id/_attr/:attr", + get(account_id_get_attr) + .put(account_id_put_attr) + .post(account_id_post_attr) + .delete(account_id_delete_attr), + ) + .route("/service_account/:id/_lock", get(|| async { "TODO" })) + .route( + "/service_account/:id/_into_person", + post(service_account_into_person), + ) + .route( + "/service_account/:id/_api_token", + post(service_account_api_token_post).get(service_account_api_token_get), + ) + .route( + "/service_account/:id/_api_token/:token_id", + delete(service_account_api_token_delete), + ) + .route("/service_account/:id/_credential", get(|| async { "TODO" })) + .route( + "/service_account/:id/_credential/_generate", + get(service_account_credential_generate), + ) + .route( + "/service_account/:id/_credential/_status", + get(account_get_id_credential_status), + ) + .route( + "/service_account/:id/_credential/:cid/_lock", + get(|| async { "TODO" }), + ) + .route( + "/service_account/:id/_ssh_pubkeys", + get(account_get_id_ssh_pubkeys).post(account_post_id_ssh_pubkey), + ) + .route( + "/service_account/:id/_ssh_pubkeys/:tag", + get(account_get_id_ssh_pubkey_tag).delete(account_delete_id_ssh_pubkey_tag), + ) + .route("/service_account/:id/_unix", post(account_post_id_unix)) + .route("/account/:id/_unix/_auth", post(account_post_id_unix_auth)) + .route( + "/account/:id/_unix/_token", + post(account_get_id_unix_token).get(account_get_id_unix_token), // TODO: make this cacheable + ) + .route( + "/account/:id/_radius/_token", + post(account_get_id_radius_token).get(account_get_id_radius_token), // TODO: make this cacheable + ) + .route("/account/:id/_ssh_pubkeys", get(account_get_id_ssh_pubkeys)) + .route( + "/account/:id/_ssh_pubkeys/:tag", + get(account_get_id_ssh_pubkey_tag), + ) + .route( + "/account/:id/_user_auth_token", + get(account_get_id_user_auth_token), + ) + .route( + "/account/:id/_user_auth_token/:token_id", + delete(account_user_auth_token_delete), + ) + .route( + "/credential/_exchange_intent", + post(credential_update_exchange_intent), + ) + .route("/credential/_status", post(credential_update_status)) + .route("/credential/_update", post(credential_update_update)) + .route("/credential/_commit", post(credential_update_commit)) + .route("/credential/_cancel", post(credential_update_cancel)) + // domain-things + .route("/domain", get(domain_get)) + .route( + "/domain/_attr/:attr", + get(domain_get_attr) + .put(domain_put_attr) + .delete(domain_delete_attr), + ) + .route("/group/:id/_unix/_token", get(group_get_id_unix_token)) + .route("/group/:id/_unix", post(group_post_id_unix)) + .route("/group", get(group_get).post(group_post)) + .route("/group/:id", get(group_id_get).delete(group_id_delete)) + .route( + "/group/:id/_attr/:attr", + delete(group_id_delete_attr) + .get(group_id_get_attr) + .put(group_id_put_attr) + .post(group_id_post_attr), + ) + .with_state(state.clone()) + .route("/system", get(system_get)) + .route( + "/system/_attr/:attr", + get(system_get_attr) + .post(system_post_attr) + .delete(system_delete_attr), + ) + .route("/recycle_bin", get(recycle_bin_get)) + .route("/recycle_bin/:id", get(recycle_bin_id_get)) + .route("/recycle_bin/:id/_revive", post(recycle_bin_revive_id_post)) + .route("/access_profile", get(|| async { "TODO" })) + .route("/access_profile/:id", get(|| async { "TODO" })) + .route("/access_profile/:id/_attr/:attr", get(|| async { "TODO" })) + .route("/auth", post(auth)) + .route("/auth/valid", get(auth_valid)) + .route("/logout", get(logout)) + .route("/reauth", post(reauth)) + .route( + "/sync_account", + get(sync_account_get).post(sync_account_post), + ) + .route( + "/sync_account/", + get(sync_account_get).post(sync_account_post), + ) + .route( + "/sync_account/:id", + get(sync_account_id_get).patch(sync_account_id_patch), + ) + .route( + "/sync_account/:id/_attr/:attr", + get(sync_account_id_get_attr).put(sync_account_id_put_attr), + ) + .route( + "/sync_account/:id/_finalise", + get(sync_account_id_get_finalise), + ) + .route( + "/sync_account/:id/_terminate", + get(sync_account_id_get_terminate), + ) + .route( + "/sync_account/:id/_sync_token", + // .get(&mut sync_account_token_get) + post(sync_account_token_post).delete(sync_account_token_delete), + ) + .with_state(state) + .layer(from_fn(dont_cache_me)) } diff --git a/server/core/src/https/v1_scim.rs b/server/core/src/https/v1_scim.rs index e9209a435..20b339217 100644 --- a/server/core/src/https/v1_scim.rs +++ b/server/core/src/https/v1_scim.rs @@ -1,5 +1,10 @@ -use super::routemaps::{RouteMap, RouteMaps}; -use super::{to_tide_response, AppState, RequestExtensions}; +use super::middleware::KOpId; +use super::{to_axum_response, ServerState}; +use axum::extract::{Path, State}; +use axum::response::IntoResponse; +use axum::routing::{get, post}; +use axum::{Extension, Json, Router}; +use axum_auth::AuthBearer; use kanidm_proto::scim_v1::ScimSyncRequest; use kanidm_proto::v1::Entry as ProtoEntry; use kanidmd_lib::prelude::*; @@ -9,290 +14,246 @@ use super::v1::{ json_rest_event_put_id_attr, }; -pub async fn sync_account_get(req: tide::Request) -> tide::Result { +pub async fn sync_account_get( + State(state): State, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); - json_rest_event_get(req, filter, None).await + json_rest_event_get(state, None, filter, kopid).await } -pub async fn sync_account_post(req: tide::Request) -> tide::Result { +pub async fn sync_account_post( + State(state): State, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { let classes = vec!["sync_account".to_string(), "object".to_string()]; - json_rest_event_post(req, classes).await + json_rest_event_post(state, classes, obj, kopid).await } -pub async fn sync_account_id_get(req: tide::Request) -> tide::Result { +pub async fn sync_account_id_get( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); - json_rest_event_get_id(req, filter, None).await + json_rest_event_get_id(state, id, filter, None, kopid).await } -pub async fn sync_account_id_get_attr(req: tide::Request) -> tide::Result { - let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); - json_rest_event_get_id_attr(req, filter).await -} - -pub async fn sync_account_id_put_attr(req: tide::Request) -> tide::Result { - let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); - json_rest_event_put_id_attr(req, filter).await -} - -pub async fn sync_account_id_patch(mut req: tide::Request) -> tide::Result { - // Update a value / attrs - let uat = req.get_current_uat(); - let id = req.get_url_param("id")?; - - let obj: ProtoEntry = req.body_json().await?; - +pub async fn sync_account_id_patch( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str()))); - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() + let res = state .qe_w_ref - .handle_internalpatch(uat, filter, obj, eventid) + .handle_internalpatch(kopid.uat, filter, obj, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn sync_account_id_get_finalise(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn sync_account_id_get_finalise( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_sync_account_finalise(uat, uuid_or_name, eventid) + .handle_sync_account_finalise(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn sync_account_id_get_terminate(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn sync_account_id_get_terminate( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_sync_account_terminate(uat, uuid_or_name, eventid) + .handle_sync_account_terminate(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn sync_account_token_post(mut req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let label: String = req.body_json().await?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn sync_account_token_post( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, + Json(label): Json, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_sync_account_token_generate(uat, uuid_or_name, label, eventid) + .handle_sync_account_token_generate(kopid.uat, id, label, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -pub async fn sync_account_token_delete(req: tide::Request) -> tide::Result { - let uat = req.get_current_uat(); - let uuid_or_name = req.get_url_param("id")?; - - let (eventid, hvalue) = req.new_eventid(); - - let res = req - .state() +pub async fn sync_account_token_delete( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_sync_account_token_destroy(uat, uuid_or_name, eventid) + .handle_sync_account_token_destroy(kopid.uat, id, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -async fn scim_sync_post(mut req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - - // Given the token, and a sync update, apply the changes if any - let bearer = req.get_auth_bearer(); - - // Change this type later. - let changes: ScimSyncRequest = req.body_json().await?; - - let res = req - .state() +async fn scim_sync_post( + State(state): State, + Extension(kopid): Extension, + AuthBearer(bearer): AuthBearer, + Json(changes): Json, +) -> impl IntoResponse { + let res = state .qe_w_ref - .handle_scim_sync_apply(bearer, changes, eventid) + .handle_scim_sync_apply(Some(bearer), changes, kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -async fn scim_sync_get(req: tide::Request) -> tide::Result { - let (eventid, hvalue) = req.new_eventid(); - +async fn scim_sync_get( + State(state): State, + Extension(kopid): Extension, + AuthBearer(bearer): AuthBearer, +) -> impl IntoResponse { // Given the token, what is it's connected sync state? - let bearer = req.get_auth_bearer(); trace!(?bearer); - - let res = req - .state() + let res = state .qe_r_ref - .handle_scim_sync_status(bearer, eventid) + .handle_scim_sync_status(Some(bearer), kopid.eventid) .await; - to_tide_response(res, hvalue) + to_axum_response(res) } -async fn scim_sink_get(req: tide::Request) -> tide::Result { - let (_, hvalue) = req.new_eventid(); - let mut res = tide::Response::new(200); - - res.insert_header("X-KANIDM-OPID", hvalue); - res.set_content_type("text/html;charset=utf-8"); - - res.set_body( - r#" - - - - - - - Sink! - - - - -
-                    ___
-                  .' _ '.
-                 / /` `\ \
-                 | |   [__]
-                 | |    {{
-                 | |    }}
-              _  | |  _ {{
-  ___________<_>_| |_<_>}}________
-      .=======^=(___)=^={{====.
-     / .----------------}}---. \
-    / /                 {{    \ \
-   / /                  }}     \ \
-  (  '========================='  )
-   '-----------------------------'
-        
- -"#, - ); - - Ok(res) +pub async fn sync_account_id_get_attr( + State(state): State, + Extension(kopid): Extension, + Path((id, attr)): Path<(String, String)>, +) -> impl IntoResponse { + let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); + json_rest_event_get_id_attr(state, id, attr, filter, kopid).await } -pub fn scim_route_setup(appserver: &mut tide::Route<'_, AppState>, routemap: &mut RouteMap) { - let mut scim_process = appserver.at("/scim/v1"); - - // https://datatracker.ietf.org/doc/html/rfc7644#section-3.2 - // - // HTTP SCIM Usage - // Method - // ------ -------------------------------------------------------------- - // GET Retrieves one or more complete or partial resources. - // - // POST Depending on the endpoint, creates new resources, creates a - // search request, or MAY be used to bulk-modify resources. - // - // PUT Modifies a resource by replacing existing attributes with a - // specified set of replacement attributes (replace). PUT - // MUST NOT be used to create new resources. - // - // PATCH Modifies a resource with a set of client-specified changes - // (partial update). - // - // DELETE Deletes a resource. - // - // Resource Endpoint Operations Description - // -------- ---------------- ---------------------- -------------------- - // User /Users GET (Section 3.4.1), Retrieve, add, - // POST (Section 3.3), modify Users. - // PUT (Section 3.5.1), - // PATCH (Section 3.5.2), - // DELETE (Section 3.6) - // - // Group /Groups GET (Section 3.4.1), Retrieve, add, - // POST (Section 3.3), modify Groups. - // PUT (Section 3.5.1), - // PATCH (Section 3.5.2), - // DELETE (Section 3.6) - // - // Self /Me GET, POST, PUT, PATCH, Alias for operations - // DELETE (Section 3.11) against a resource - // mapped to an - // authenticated - // subject (e.g., - // User). - // - // Service /ServiceProvider GET (Section 4) Retrieve service - // provider Config provider's - // config. configuration. - // - // Resource /ResourceTypes GET (Section 4) Retrieve supported - // type resource types. - // - // Schema /Schemas GET (Section 4) Retrieve one or more - // supported schemas. - // - // Bulk /Bulk POST (Section 3.7) Bulk updates to one - // or more resources. - // - // Search [prefix]/.search POST (Section 3.4.3) Search from system - // root or within a - // resource endpoint - // for one or more - // resource types using - // POST. - // -- Kanidm Resources - // - // Sync /Sync GET Retrieve the current - // sync state associated - // with the authenticated - // session - // - // POST Send a sync update - // - - scim_process - .at("/Sync") - .mapped_post(routemap, scim_sync_post) - .mapped_get(routemap, scim_sync_get); - - scim_process.at("/Sink").mapped_get(routemap, scim_sink_get); - - let mut sync_account_route = appserver.at("/v1/sync_account"); - sync_account_route - .at("/") - .mapped_get(routemap, sync_account_get) - .mapped_post(routemap, sync_account_post); - - sync_account_route - .at("/:id") - .mapped_get(routemap, sync_account_id_get) - .mapped_patch(routemap, sync_account_id_patch); - - sync_account_route - .at("/:id/_attr/:attr") - .mapped_get(routemap, sync_account_id_get_attr) - .mapped_put(routemap, sync_account_id_put_attr); - - sync_account_route - .at("/:id/_finalise") - .mapped_get(routemap, sync_account_id_get_finalise); - - sync_account_route - .at("/:id/_terminate") - .mapped_get(routemap, sync_account_id_get_terminate); - - sync_account_route - .at("/:id/_sync_token") - // .mapped_get(&mut routemap, sync_account_token_get) - .mapped_post(routemap, sync_account_token_post) - .mapped_delete(routemap, sync_account_token_delete); +pub async fn sync_account_id_put_attr( + State(state): State, + Extension(kopid): Extension, + Path((id, attr)): Path<(String, String)>, + Json(values): Json>, +) -> impl IntoResponse { + let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); + json_rest_event_put_id_attr(state, id, attr, filter, values, kopid).await +} + +async fn scim_sink_get() -> impl IntoResponse { + r#" + + + + + + + Sink! + + + + +
+                        ___
+                      .' _ '.
+                     / /` `\ \
+                     | |   [__]
+                     | |    {{
+                     | |    }}
+                  _  | |  _ {{
+      ___________<_>_| |_<_>}}________
+          .=======^=(___)=^={{====.
+         / .----------------}}---. \
+        / /                 {{    \ \
+       / /                  }}     \ \
+      (  '========================='  )
+       '-----------------------------'
+            
+ + "# +} + +pub fn scim_route_setup() -> Router { + Router::new() + // https://datatracker.ietf.org/doc/html/rfc7644#section-3.2 + // + // HTTP SCIM Usage + // Method + // ------ -------------------------------------------------------------- + // GET Retrieves one or more complete or partial resources. + // + // POST Depending on the endpoint, creates new resources, creates a + // search request, or MAY be used to bulk-modify resources. + // + // PUT Modifies a resource by replacing existing attributes with a + // specified set of replacement attributes (replace). PUT + // MUST NOT be used to create new resources. + // + // PATCH Modifies a resource with a set of client-specified changes + // (partial update). + // + // DELETE Deletes a resource. + // + // Resource Endpoint Operations Description + // -------- ---------------- ---------------------- -------------------- + // User /Users GET (Section 3.4.1), Retrieve, add, + // POST (Section 3.3), modify Users. + // PUT (Section 3.5.1), + // PATCH (Section 3.5.2), + // DELETE (Section 3.6) + // + // Group /Groups GET (Section 3.4.1), Retrieve, add, + // POST (Section 3.3), modify Groups. + // PUT (Section 3.5.1), + // PATCH (Section 3.5.2), + // DELETE (Section 3.6) + // + // Self /Me GET, POST, PUT, PATCH, Alias for operations + // DELETE (Section 3.11) against a resource + // mapped to an + // authenticated + // subject (e.g., + // User). + // + // Service /ServiceProvider GET (Section 4) Retrieve service + // provider Config provider's + // config. configuration. + // + // Resource /ResourceTypes GET (Section 4) Retrieve supported + // type resource types. + // + // Schema /Schemas GET (Section 4) Retrieve one or more + // supported schemas. + // + // Bulk /Bulk POST (Section 3.7) Bulk updates to one + // or more resources. + // + // Search [prefix]/.search POST (Section 3.4.3) Search from system + // root or within a + // resource endpoint + // for one or more + // resource types using + // POST. + // -- Kanidm Resources + // + // Sync /Sync GET Retrieve the current + // sync state associated + // with the authenticated + // session + // + // POST Send a sync update + // + .route("/v1/Sync", post(scim_sync_post).get(scim_sync_get)) + .route("/v1/Sink", get(scim_sink_get)) } diff --git a/server/core/src/lib.rs b/server/core/src/lib.rs index 1828e49bd..b45f0437d 100644 --- a/server/core/src/lib.rs +++ b/server/core/src/lib.rs @@ -28,7 +28,7 @@ extern crate kanidmd_lib; pub mod actors; pub mod config; mod crypto; -pub mod https; +mod https; mod interval; mod ldaps; @@ -911,26 +911,27 @@ pub async fn create_server_core( admin_info!("this config rocks! 🪨 "); None } else { - // ⚠️ only start the sockets and listeners in non-config-test modes. - let h = self::https::create_https_server( - config.address, - &config.domain, - config.tls_config.as_ref(), - config.role, - config.trust_x_forward_for, - &cookie_key, + let h: tokio::task::JoinHandle<()> = match https::create_https_server( + config.clone(), + cookie_key, jws_signer, status_ref, server_write_ref, server_read_ref, broadcast_tx.subscribe(), ) - .await?; - + .await + { + Ok(h) => h, + Err(e) => { + error!("Failed to start HTTPS server -> {:?}", e); + return Err(()); + } + }; if config.role != ServerRole::WriteReplicaNoUI { - admin_info!("ready to rock! 🪨 UI available at: {}", config.origin); + admin_info!("ready to rock! 🪨 UI available at: {}", config.origin); } else { - admin_info!("ready to rock! 🪨"); + admin_info!("ready to rock! 🪨 "); } Some(h) }; diff --git a/server/daemon/run_insecure_dev_server.sh b/server/daemon/run_insecure_dev_server.sh index 074ed5fd8..190ca2fde 100755 --- a/server/daemon/run_insecure_dev_server.sh +++ b/server/daemon/run_insecure_dev_server.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -e + # This script based on the developer readme and allows you to run a test server. if [ -z "$KANI_CARGO_OPTS" ]; then diff --git a/server/daemon/src/main.rs b/server/daemon/src/main.rs index efeccf0a8..29668700f 100644 --- a/server/daemon/src/main.rs +++ b/server/daemon/src/main.rs @@ -106,8 +106,8 @@ async fn main() -> ExitCode { let mut config = Configuration::new(); // Check the permissions are OK. let cfg_path = &opt.commands.commonopt().config_path; // TODO: this needs to be pulling from the default or something? - if format!("{}", cfg_path.display()) == "".to_string() { - config_error.push(format!("Refusing to run - config file path is empty")); + if cfg_path.display().to_string().is_empty() { + config_error.push("Refusing to run - config file path is empty".to_string()); } if !cfg_path.exists() { config_error.push(format!( @@ -149,7 +149,7 @@ async fn main() -> ExitCode { // Fall back to stderr .map_sender(|sender| sender.or_stderr()) .build_on(|subscriber|{ - let sub = subscriber.with(log_filter); + subscriber.with(log_filter) // this does NOT work, it just adds a layer. // if std::io::stdout().is_terminal() { // println!("Stdout is a terminal"); @@ -158,7 +158,6 @@ async fn main() -> ExitCode { // println!("Stdout is not a terminal"); // sub.with(sketching::tracing_subscriber::fmt::layer().with_writer(std::io::stderr)) // } - sub }) .on(async { // Get information on the windows username @@ -200,7 +199,13 @@ async fn main() -> ExitCode { return ExitCode::SUCCESS }; - let sconfig = sconfig.expect("Somehow you got an empty ServerConfig after error checking?"); + let sconfig = match sconfig { + Some(val) => val, + None => { + error!("Somehow you got an empty ServerConfig after error checking?"); + return ExitCode::FAILURE + } + }; #[cfg(target_family = "unix")] @@ -313,7 +318,7 @@ async fn main() -> ExitCode { /* // Apply any cli overrides, normally debug level. if opt.commands.commonopt().debug.as_ref() { - // ::std::env::set_var("RUST_LOG", "tide=info,kanidm=info,webauthn=debug"); + // ::std::env::set_var("RUST_LOG", ",kanidm=info,webauthn=debug"); } */ @@ -383,30 +388,35 @@ async fn main() -> ExitCode { } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::terminate(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { break } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::alarm(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::hangup(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined1(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined2(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore @@ -554,17 +564,33 @@ async fn main() -> ExitCode { let ca_cert_path = PathBuf::from(ca_cert); match ca_cert_path.exists() { true => { - let ca_contents = std::fs::read_to_string(ca_cert_path.clone()).expect(&format!("Failed to read {}!", ca_cert)); + let ca_contents = match std::fs::read_to_string(ca_cert_path.clone()) { + Ok(val) => val, + Err(e) => { + error!("Failed to read {:?} from filesystem: {:?}", ca_cert_path, e); + return ExitCode::FAILURE + } + }; let content = ca_contents .split("-----END CERTIFICATE-----") - .into_iter() .filter_map(|c| if c.trim().is_empty() { None } else { Some(c.trim().to_string())}) .collect::>(); - let content = content.last().expect(&format!("Failed to pull the last chunk of {} as a valid certificate!", ca_cert)); + let content = match content.last() { + Some(val) => val, + None => { + error!("Failed to parse {:?} as valid certificate", ca_cert_path); + return ExitCode::FAILURE + } + }; let content = format!("{}-----END CERTIFICATE-----", content); - let ca_cert_parsed = reqwest::Certificate::from_pem(content.as_bytes()) - .expect(&format!("Failed to parse {} as a valid certificate!\n{}", ca_cert, content)); + let ca_cert_parsed = match reqwest::Certificate::from_pem(content.as_bytes()) { + Ok(val) => val, + Err(e) =>{ + error!("Failed to parse {} as a valid certificate!\nError: {:?}", ca_cert, e); + return ExitCode::FAILURE + } + }; client.add_root_certificate(ca_cert_parsed) }, false => { @@ -574,7 +600,7 @@ async fn main() -> ExitCode { } } }; - + #[allow(clippy::unwrap_used)] let client = client .build() .unwrap(); diff --git a/server/lib/Cargo.toml b/server/lib/Cargo.toml index 616f0ce3d..1baa8993c 100644 --- a/server/lib/Cargo.toml +++ b/server/lib/Cargo.toml @@ -51,7 +51,6 @@ sketching = { workspace = true } smartstring = { workspace = true, features = ["serde"] } smolset = { workspace = true } sshkeys = { workspace = true } -tide = { workspace = true } time = { workspace = true, features = ["serde", "std"] } tokio = { workspace = true, features = ["net", "sync", "time", "rt"] } tokio-util = { workspace = true, features = ["codec"] } diff --git a/server/lib/src/idm/credupdatesession.rs b/server/lib/src/idm/credupdatesession.rs index 2647a3c79..9627e57d0 100644 --- a/server/lib/src/idm/credupdatesession.rs +++ b/server/lib/src/idm/credupdatesession.rs @@ -440,7 +440,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // At lease *one* must be modifiable OR visible. if !(primary_can_edit || passkeys_can_edit || ext_cred_portal_can_view) { error!("Unable to proceed with credential update intent - at least one type of credential must be modifiable or visible."); - return Err(OperationError::NotAuthorised); + Err(OperationError::NotAuthorised) } else { security_info!(%primary_can_edit, %passkeys_can_edit, %ext_cred_portal_can_view, "Proceeding"); Ok(( @@ -459,7 +459,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { sessionid: Uuid, intent_token_id: Option, account: Account, - perms: &CredUpdateSessionPerms, + perms: CredUpdateSessionPerms, ct: Duration, ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> { let ext_cred_portal_can_view = perms.ext_cred_portal_can_view; @@ -809,7 +809,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // ========== // Okay, good to exchange. - self.create_credupdate_session(session_id, Some(intent_id), account, &perms, current_time) + self.create_credupdate_session(session_id, Some(intent_id), account, perms, current_time) } #[instrument(level = "debug", skip_all)] @@ -826,7 +826,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { let sessionid = uuid_from_duration(ct + MAXIMUM_CRED_UPDATE_TTL, self.sid); // Build the cred update session. - self.create_credupdate_session(sessionid, None, account, &perms, ct) + self.create_credupdate_session(sessionid, None, account, perms, ct) } #[instrument(level = "trace", skip(self))] @@ -1141,6 +1141,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> { Ok(status) } + #[instrument(level = "debug", skip(self))] fn check_password_quality( &self, cleartext: &str, @@ -1284,6 +1285,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> { } } + #[instrument(level = "trace", skip(cust, self))] pub fn credential_primary_set_password( &self, cust: &CredentialUpdateSessionToken, diff --git a/server/lib/src/idm/oauth2.rs b/server/lib/src/idm/oauth2.rs index d898cb069..9889a5c5e 100644 --- a/server/lib/src/idm/oauth2.rs +++ b/server/lib/src/idm/oauth2.rs @@ -1796,6 +1796,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> { } } +// TODO: this can be handled by the auth header parsers in axum fn parse_basic_authz(client_authz: &str) -> Result<(String, String), Oauth2Error> { // Check the client_authz let authz = general_purpose::STANDARD diff --git a/server/lib/src/plugins/namehistory.rs b/server/lib/src/plugins/namehistory.rs index 7d2030a20..6942dbfe3 100644 --- a/server/lib/src/plugins/namehistory.rs +++ b/server/lib/src/plugins/namehistory.rs @@ -63,7 +63,7 @@ impl NameHistory { } fn handle_name_creation( - cands: &mut Vec, + cands: &mut [EntryInvalidNew], cid: &Cid, ) -> Result<(), OperationError> { for cand in cands.iter_mut() { diff --git a/server/lib/src/schema.rs b/server/lib/src/schema.rs index e49320e2d..6d62dbce6 100644 --- a/server/lib/src/schema.rs +++ b/server/lib/src/schema.rs @@ -1923,7 +1923,7 @@ impl Schema { } } - #[cfg(any(test))] + #[cfg(test)] pub(crate) fn write_blocking(&self) -> SchemaWriteTransaction<'_> { self.write() } diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index b27464075..a06f6824c 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -424,7 +424,7 @@ impl<'a> QueryServerWriteTransaction<'a> { .into_iter() .try_for_each(|entry| self.internal_migrate_or_create(entry)); - if !r.is_ok() { + if r.is_err() { error!(res = ?r, "initialise_schema_idm -> Error"); } debug_assert!(r.is_ok()); diff --git a/server/testkit/Cargo.toml b/server/testkit/Cargo.toml index 037a4037e..7c392b859 100644 --- a/server/testkit/Cargo.toml +++ b/server/testkit/Cargo.toml @@ -23,7 +23,7 @@ kanidmd_lib = { workspace = true } url = { workspace = true, features = ["serde"] } -reqwest = { workspace = true, default-features = false } +reqwest = { workspace = true, default-features = false, features=["cookies"] } sketching = { workspace = true } testkit-macros = { workspace = true } tracing = { workspace = true, features = ["attributes"] } diff --git a/server/testkit/src/lib.rs b/server/testkit/src/lib.rs index 94183b295..c6f8f7ea6 100644 --- a/server/testkit/src/lib.rs +++ b/server/testkit/src/lib.rs @@ -20,16 +20,16 @@ use tokio::task; pub const ADMIN_TEST_USER: &str = "admin"; pub const ADMIN_TEST_PASSWORD: &str = "integration test admin password"; + +pub const NOT_ADMIN_TEST_USERNAME: &str = "krab_test_user"; +pub const NOT_ADMIN_TEST_PASSWORD: &str = "eicieY7ahchaoCh0eeTa"; + pub static PORT_ALLOC: AtomicU16 = AtomicU16::new(18080); pub use testkit_macros::test; pub fn is_free_port(port: u16) -> bool { - // TODO: Refactor to use `Result::is_err` in a future PR - match TcpStream::connect(("0.0.0.0", port)) { - Ok(_) => false, - Err(_) => true, - } + TcpStream::connect(("0.0.0.0", port)).is_err() } // Test external behaviours of the service. @@ -46,9 +46,10 @@ pub async fn setup_async_test() -> (KanidmClient, CoreHandle) { break possible_port; } counter += 1; + #[allow(clippy::panic)] if counter >= 5 { eprintln!("Unable to allocate port!"); - assert!(false); + panic!(); } }; @@ -67,21 +68,25 @@ pub async fn setup_async_test() -> (KanidmClient, CoreHandle) { config.role = ServerRole::WriteReplica; config.domain = "localhost".to_string(); config.origin = addr.clone(); - // config.log_level = Some(LogLevel::Verbose as u32); - // config.log_level = Some(LogLevel::FullTrace as u32); config.threads = 1; - let core_handle = create_server_core(config, false) - .await - .expect("failed to start server core"); - // We have to yield now to guarantee that the tide elements are setup. + let core_handle = match create_server_core(config, false).await { + Ok(val) => val, + #[allow(clippy::panic)] + Err(_) => panic!("failed to start server core"), + }; + // We have to yield now to guarantee that the elements are setup. task::yield_now().await; - let rsclient = KanidmClientBuilder::new() + #[allow(clippy::panic)] + let rsclient = match KanidmClientBuilder::new() .address(addr.clone()) .no_proxy() .build() - .expect("Failed to build client"); + { + Ok(val) => val, + Err(_) => panic!("failed to build client"), + }; tracing::info!("Testkit server setup complete - {}", addr); diff --git a/server/testkit/tests/default_entries.rs b/server/testkit/tests/default_entries.rs index f39bac309..3159dd003 100644 --- a/server/testkit/tests/default_entries.rs +++ b/server/testkit/tests/default_entries.rs @@ -4,7 +4,7 @@ use std::collections::HashSet; use kanidm_client::KanidmClient; use kanidm_proto::v1::{Filter, Modify, ModifyList}; -use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER}; +use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER, NOT_ADMIN_TEST_PASSWORD}; static USER_READABLE_ATTRS: [&str; 9] = [ "name", @@ -195,13 +195,13 @@ async fn login_account(rsclient: &KanidmClient, id: &str) { .unwrap(); rsclient - .idm_person_account_primary_credential_set_password(id, "eicieY7ahchaoCh0eeTa") + .idm_person_account_primary_credential_set_password(id, NOT_ADMIN_TEST_PASSWORD) .await .unwrap(); let _ = rsclient.logout(); let res = rsclient - .auth_simple_password(id, "eicieY7ahchaoCh0eeTa") + .auth_simple_password(id, NOT_ADMIN_TEST_PASSWORD) .await; // Setup privs @@ -209,7 +209,7 @@ async fn login_account(rsclient: &KanidmClient, id: &str) { assert!(res.is_ok()); let res = rsclient - .reauth_simple_password("eicieY7ahchaoCh0eeTa") + .reauth_simple_password(NOT_ADMIN_TEST_PASSWORD) .await; println!("{} priv granted for", id); assert!(res.is_ok()); @@ -758,3 +758,124 @@ async fn test_self_write_mail_priv_people(rsclient: KanidmClient) { login_account_via_admin(&rsclient, "nonperson").await; test_write_attrs(&rsclient, "nonperson", &["mail"], false).await; } + +#[kanidmd_testkit::test] +async fn test_https_robots_txt(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + + let response = match reqwest::get(format!("{}/robots.txt", &addr)).await { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 200); + + eprintln!( + "csp headers: {:#?}", + response.headers().get("content-security-policy") + ); + assert_ne!(response.headers().get("content-security-policy"), None); + eprintln!("{}", response.text().await.unwrap()); +} + +// TODO: #1787 when the routemap comes back +// #[kanidmd_testkit::test] +// async fn test_https_routemap(rsclient: KanidmClient) { +// // We need to do manual reqwests here. +// let addr = rsclient.get_url(); + +// let response = match reqwest::get(format!("{}/v1/routemap", &addr)).await { +// Ok(value) => value, +// Err(error) => { +// panic!("Failed to query {:?} : {:#?}", addr, error); +// } +// }; +// eprintln!("response: {:#?}", response); +// assert_eq!(response.status(), 200); + +// let body = response.text().await.unwrap(); +// eprintln!("{}", body); +// assert!(body.contains("/scim/v1/Sync")); +// assert!(body.contains(r#""path": "/v1/routemap""#)); +// } + +/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better... +#[kanidmd_testkit::test] +async fn test_v1_raw_delete(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + let post_body = serde_json::json!({"filter": "self"}).to_string(); + + let response = match client + .post(format!("{}/v1/raw/delete", &addr)) + .header("Content-Type", "application/json") + .body(post_body) + .send() + .await + { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 401); + + let body = response.text().await.unwrap(); + eprintln!("{}", body); +} + +/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better... +#[kanidmd_testkit::test] +async fn test_v1_raw_logout(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + let response = match client.get(format!("{}/v1/logout", &addr)).send().await { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 401); + + let body = response.text().await.unwrap(); + eprintln!("{}", body); +} + +/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better... +#[kanidmd_testkit::test] +async fn test_status_endpoint(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + let response = match client.get(format!("{}/status", &addr)).send().await { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 200); + + let body = response.text().await.unwrap(); + eprintln!("{}", body); + assert!(body.contains("true") == true); +} diff --git a/server/testkit/tests/http_manifest.rs b/server/testkit/tests/http_manifest.rs new file mode 100644 index 000000000..0c9a9e32d --- /dev/null +++ b/server/testkit/tests/http_manifest.rs @@ -0,0 +1,22 @@ +use kanidm_client::KanidmClient; + +#[kanidmd_testkit::test] +async fn test_https_manifest(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + + // here we test the /ui/ endpoint which should have the headers + let response = match reqwest::get(format!("{}/manifest.webmanifest", &addr)).await { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 200); + + eprintln!( + "csp headers: {:#?}", + response.headers().get("content-security-policy") + ); +} diff --git a/server/testkit/tests/https_middleware.rs b/server/testkit/tests/https_middleware.rs index 731cb0692..535379164 100644 --- a/server/testkit/tests/https_middleware.rs +++ b/server/testkit/tests/https_middleware.rs @@ -14,26 +14,25 @@ async fn test_https_middleware_headers(rsclient: KanidmClient) { }; eprintln!("response: {:#?}", response); assert_eq!(response.status(), 200); - eprintln!( "csp headers: {:#?}", response.headers().get("content-security-policy") ); assert_ne!(response.headers().get("content-security-policy"), None); - // here we test the /pkg/ endpoint which shouldn't have the headers - let response = - match reqwest::get(format!("{}/pkg/external/bootstrap.bundle.min.js", &addr)).await { - Ok(value) => value, - Err(error) => { - panic!("Failed to query {:?} : {:#?}", addr, error); - } - }; + // here we test the /ui/login endpoint which should have the headers + let response = match reqwest::get(format!("{}/ui/login", &addr)).await { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; eprintln!("response: {:#?}", response); assert_eq!(response.status(), 200); + eprintln!( "csp headers: {:#?}", response.headers().get("content-security-policy") ); - assert_eq!(response.headers().get("content-security-policy"), None); + assert_ne!(response.headers().get("content-security-policy"), None); } diff --git a/server/testkit/tests/oauth2_test.rs b/server/testkit/tests/oauth2_test.rs index 0b5d9c13e..b60a62116 100644 --- a/server/testkit/tests/oauth2_test.rs +++ b/server/testkit/tests/oauth2_test.rs @@ -9,6 +9,7 @@ use kanidm_proto::oauth2::{ AccessTokenResponse, AuthorisationResponse, GrantTypeReq, OidcDiscoveryResponse, }; use oauth2_ext::PkceCodeChallenge; +use reqwest::StatusCode; use url::Url; use kanidm_client::KanidmClient; @@ -38,6 +39,11 @@ macro_rules! assert_no_cache { }}; } +const TEST_INTEGRATION_RS_ID: &str = "test_integration"; +const TEST_INTEGRATION_RS_GROUP_ALL: &str = "idm_all_accounts"; +const TEST_INTEGRATION_RS_DISPLAY: &str = "Test Integration"; +const TEST_INTEGRATION_RS_URL: &str = "https://demo.example.com"; + #[kanidmd_testkit::test] async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) { let res = rsclient @@ -48,9 +54,9 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) { // Create an oauth2 application integration. rsclient .idm_oauth2_rs_basic_create( - "test_integration", - "Test Integration", - "https://demo.example.com", + TEST_INTEGRATION_RS_ID, + TEST_INTEGRATION_RS_DISPLAY, + TEST_INTEGRATION_RS_URL, ) .await .expect("Failed to create oauth2 config"); @@ -366,4 +372,110 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) { eprintln!("oidc {oidc:?}"); assert!(userinfo == oidc); + + // auth back with admin so we can test deleting things + let res = rsclient + .auth_simple_password("admin", ADMIN_TEST_PASSWORD) + .await; + assert!(res.is_ok()); + rsclient + .idm_oauth2_rs_delete_sup_scope_map("test_integration", TEST_INTEGRATION_RS_GROUP_ALL) + .await + .expect("Failed to update oauth2 scopes"); +} + +#[kanidmd_testkit::test] +async fn test_oauth2_token_post_bad_bodies(rsclient: KanidmClient) { + let res = rsclient + .auth_simple_password("admin", ADMIN_TEST_PASSWORD) + .await; + assert!(res.is_ok()); + + let url = rsclient.get_url().to_string(); + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .no_proxy() + .build() + .expect("Failed to create client."); + + // test for a bad-body request on token + let response = client + .post(format!("{}/oauth2/token", url)) + .form(&serde_json::json!({})) + // .bearer_auth(atr.access_token.clone()) + .send() + .await + .expect("Failed to send token request."); + println!("{:?}", response); + assert!(response.status() == StatusCode::UNPROCESSABLE_ENTITY); + + // test for a bad-auth request + let response = client + .post(format!("{}/oauth2/token/introspect", url)) + .form(&serde_json::json!({ "token": "lol" })) + .send() + .await + .expect("Failed to send token introspection request."); + println!("{:?}", response); + assert!(response.status() == StatusCode::UNAUTHORIZED); +} + +#[kanidmd_testkit::test] +async fn test_oauth2_token_revoke_post(rsclient: KanidmClient) { + let res = rsclient + .auth_simple_password("admin", ADMIN_TEST_PASSWORD) + .await; + assert!(res.is_ok()); + + let url = rsclient.get_url().to_string(); + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .no_proxy() + .build() + .expect("Failed to create client."); + + // test for a bad-body request on token + let response = client + .post(format!("{}/oauth2/token/revoke", url)) + .form(&serde_json::json!({})) + .bearer_auth("lolol") + .send() + .await + .expect("Failed to send token request."); + println!("{:?}", response); + assert!(response.status() == StatusCode::UNPROCESSABLE_ENTITY); + + // test for a invalid format request on token + let response = client + .post(format!("{}/oauth2/token/revoke", url)) + .json("") + .bearer_auth("lolol") + .send() + .await + .expect("Failed to send token request."); + println!("{:?}", response); + + assert!(response.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE); + + // test for a bad-body request on token + let response = client + .post(format!("{}/oauth2/token/revoke", url)) + .form(&serde_json::json!({})) + .bearer_auth("Basic lolol") + .send() + .await + .expect("Failed to send token request."); + println!("{:?}", response); + assert!(response.status() == StatusCode::UNPROCESSABLE_ENTITY); + + // test for a bad-body request on token + let response = client + .post(format!("{}/oauth2/token/revoke", url)) + .body(serde_json::json!({}).to_string()) + .bearer_auth("Basic lolol") + .send() + .await + .expect("Failed to send token request."); + println!("{:?}", response); + assert!(response.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE); } diff --git a/server/testkit/tests/person.rs b/server/testkit/tests/person.rs new file mode 100644 index 000000000..e77ffb5e2 --- /dev/null +++ b/server/testkit/tests/person.rs @@ -0,0 +1,32 @@ +use kanidm_client::KanidmClient; + +/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better... +#[kanidmd_testkit::test] +async fn test_v1_person_patch(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + let post_body = serde_json::json!({"attrs": { "email" : "crab@example.com"}}).to_string(); + + let response = match client + .patch(format!("{}/v1/person/foo", &addr)) + .header("Content-Type", "application/json") + .body(post_body) + .send() + .await + { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 422); + + let body = response.text().await.unwrap(); + eprintln!("{}", body); +} diff --git a/server/testkit/tests/routes.rs b/server/testkit/tests/routes.rs new file mode 100644 index 000000000..8e1fb3d46 --- /dev/null +++ b/server/testkit/tests/routes.rs @@ -0,0 +1,681 @@ +use kanidm_client::KanidmClient; + +#[kanidmd_testkit::test] +async fn test_routes(rsclient: KanidmClient) { + let routemap = r#" + [ + { + "path": "/", + "method": "GET" + }, + { + "path": "/robots.txt", + "method": "GET" + }, + { + "path": "/manifest.webmanifest", + "method": "GET" + }, + { + "path": "/ui/", + "method": "GET" + }, + { + "path": "/ui/*", + "method": "GET" + }, + { + "path": "/v1/account/:id/_unix/_token", + "method": "GET" + }, + { + "path": "/v1/account/:id/_radius/_token", + "method": "GET" + }, + { + "path": "/v1/group/:id/_unix/_token", + "method": "GET" + }, + { + "path": "/v1/oauth2/:rs_name/_icon", + "method": "GET" + }, + { + "path": "/status", + "method": "GET" + }, + { + "path": "/oauth2/authorise", + "method": "POST" + }, + { + "path": "/oauth2/authorise", + "method": "GET" + }, + { + "path": "/oauth2/authorise/permit", + "method": "POST" + }, + { + "path": "/oauth2/authorise/permit", + "method": "GET" + }, + { + "path": "/oauth2/authorise/reject", + "method": "POST" + }, + { + "path": "/oauth2/authorise/reject", + "method": "GET" + }, + { + "path": "/oauth2/token", + "method": "POST" + }, + { + "path": "/oauth2/token/introspect", + "method": "POST" + }, + { + "path": "/oauth2/token/revoke", + "method": "POST" + }, + { + "path": "/oauth2/openid/:client_id/.well-known/openid-configuration", + "method": "GET" + }, + { + "path": "/oauth2/openid/:client_id/userinfo", + "method": "GET" + }, + { + "path": "/oauth2/openid/:client_id/public_key.jwk", + "method": "GET" + }, + { + "path": "/scim/v1/Sync", + "method": "POST" + }, + { + "path": "/scim/v1/Sync", + "method": "GET" + }, + { + "path": "/scim/v1/Sink", + "method": "GET" + }, + { + "path": "/v1/sync_account", + "method": "GET" + }, + { + "path": "/v1/sync_account", + "method": "POST" + }, + { + "path": "/v1/sync_account/:id", + "method": "GET" + }, + { + "path": "/v1/sync_account/:id", + "method": "PATCH" + }, + { + "path": "/v1/sync_account/:id/_finalise", + "method": "GET" + }, + { + "path": "/v1/sync_account/:id/_terminate", + "method": "GET" + }, + { + "path": "/v1/sync_account/:id/_sync_token", + "method": "POST" + }, + { + "path": "/v1/sync_account/:id/_sync_token", + "method": "DELETE" + }, + { + "path": "/v1/raw/create", + "method": "POST" + }, + { + "path": "/v1/raw/modify", + "method": "POST" + }, + { + "path": "/v1/raw/delete", + "method": "POST" + }, + { + "path": "/v1/raw/search", + "method": "POST" + }, + { + "path": "/v1/auth", + "method": "POST" + }, + { + "path": "/v1/auth/valid", + "method": "GET" + }, + { + "path": "/v1/reauth", + "method": "POST" + }, + { + "path": "/v1/logout", + "method": "GET" + }, + { + "path": "/v1/schema", + "method": "GET" + }, + { + "path": "/v1/schema/attributetype", + "method": "GET" + }, + { + "path": "/v1/schema/attributetype", + "method": "POST" + }, + { + "path": "/v1/schema/attributetype/:id", + "method": "GET" + }, + { + "path": "/v1/schema/attributetype/:id", + "method": "PUT" + }, + { + "path": "/v1/schema/attributetype/:id", + "method": "PATCH" + }, + { + "path": "/v1/schema/classtype", + "method": "GET" + }, + { + "path": "/v1/schema/classtype", + "method": "POST" + }, + { + "path": "/v1/schema/classtype/:id", + "method": "GET" + }, + { + "path": "/v1/schema/classtype/:id", + "method": "PUT" + }, + { + "path": "/v1/schema/classtype/:id", + "method": "PATCH" + }, + { + "path": "/v1/oauth2", + "method": "GET" + }, + { + "path": "/v1/oauth2/_basic", + "method": "POST" + }, + { + "path": "/v1/oauth2/:rs_name", + "method": "GET" + }, + { + "path": "/v1/oauth2/:rs_name", + "method": "PATCH" + }, + { + "path": "/v1/oauth2/:rs_name", + "method": "DELETE" + }, + { + "path": "/v1/oauth2/:rs_name/_basic_secret", + "method": "GET" + }, + { + "path": "/v1/oauth2/_scopemap/:id/:group", + "method": "POST" + }, + { + "path": "/v1/oauth2/_scopemap/:id/:group", + "method": "DELETE" + }, + { + "path": "/v1/oauth2/_sup_scopemap/:id/:group", + "method": "POST" + }, + { + "path": "/v1/oauth2/_sup_scopemap/:id/:group", + "method": "DELETE" + }, + { + "path": "/v1/self", + "method": "GET" + }, + { + "path": "/v1/self/_uat", + "method": "GET" + }, + { + "path": "/v1/self/_attr/:attr", + "method": "GET" + }, + { + "path": "/v1/self/_credential", + "method": "GET" + }, + { + "path": "/v1/self/_credential/:cid/_lock", + "method": "GET" + }, + { + "path": "/v1/self/_radius", + "method": "GET" + }, + { + "path": "/v1/self/_radius", + "method": "DELETE" + }, + { + "path": "/v1/self/_radius", + "method": "POST" + }, + { + "path": "/v1/self/_radius/_config", + "method": "POST" + }, + { + "path": "/v1/self/_radius/_config/:token", + "method": "GET" + }, + { + "path": "/v1/self/_radius/_config/:token/apple", + "method": "GET" + }, + { + "path": "/v1/self/_applinks", + "method": "GET" + }, + { + "path": "/v1/person", + "method": "GET" + }, + { + "path": "/v1/person", + "method": "POST" + }, + { + "path": "/v1/person/:id", + "method": "GET" + }, + { + "path": "/v1/person/:id", + "method": "PATCH" + }, + { + "path": "/v1/person/:id", + "method": "DELETE" + }, + { + "path": "/v1/person/:id/_attr/:attr", + "method": "GET" + }, + { + "path": "/v1/person/:id/_attr/:attr", + "method": "PUT" + }, + { + "path": "/v1/person/:id/_attr/:attr", + "method": "POST" + }, + { + "path": "/v1/person/:id/_attr/:attr", + "method": "DELETE" + }, + { + "path": "/v1/person/:id/_lock", + "method": "GET" + }, + { + "path": "/v1/person/:id/_credential", + "method": "GET" + }, + { + "path": "/v1/person/:id/_credential/_status", + "method": "GET" + }, + { + "path": "/v1/person/:id/_credential/:cid/_lock", + "method": "GET" + }, + { + "path": "/v1/person/:id/_credential/_update", + "method": "GET" + }, + { + "path": "/v1/person/:id/_credential/_update_intent", + "method": "GET" + }, + { + "path": "/v1/person/:id/_credential/_update_intent/:ttl", + "method": "GET" + }, + { + "path": "/v1/person/:id/_ssh_pubkeys", + "method": "GET" + }, + { + "path": "/v1/person/:id/_ssh_pubkeys", + "method": "POST" + }, + { + "path": "/v1/person/:id/_ssh_pubkeys/:tag", + "method": "GET" + }, + { + "path": "/v1/person/:id/_ssh_pubkeys/:tag", + "method": "DELETE" + }, + { + "path": "/v1/person/:id/_radius", + "method": "GET" + }, + { + "path": "/v1/person/:id/_radius", + "method": "POST" + }, + { + "path": "/v1/person/:id/_radius", + "method": "DELETE" + }, + { + "path": "/v1/person/:id/_unix", + "method": "POST" + }, + { + "path": "/v1/person/:id/_unix/_credential", + "method": "PUT" + }, + { + "path": "/v1/person/:id/_unix/_credential", + "method": "DELETE" + }, + { + "path": "/v1/service_account", + "method": "GET" + }, + { + "path": "/v1/service_account", + "method": "POST" + }, + { + "path": "/v1/service_account/:id", + "method": "GET" + }, + { + "path": "/v1/service_account/:id", + "method": "PATCH" + }, + { + "path": "/v1/service_account/:id", + "method": "DELETE" + }, + { + "path": "/v1/service_account/:id/_attr/:attr", + "method": "GET" + }, + { + "path": "/v1/service_account/:id/_attr/:attr", + "method": "PUT" + }, + { + "path": "/v1/service_account/:id/_attr/:attr", + "method": "POST" + }, + { + "path": "/v1/service_account/:id/_attr/:attr", + "method": "DELETE" + }, + { + "path": "/v1/service_account/:id/_lock", + "method": "GET" + }, + { + "path": "/v1/service_account/:id/_into_person", + "method": "POST" + }, + { + "path": "/v1/service_account/:id/_api_token", + "method": "POST" + }, + { + "path": "/v1/service_account/:id/_api_token", + "method": "GET" + }, + { + "path": "/v1/service_account/:id/_api_token/:token_id", + "method": "DELETE" + }, + { + "path": "/v1/service_account/:id/_credential", + "method": "GET" + }, + { + "path": "/v1/service_account/:id/_credential/_generate", + "method": "GET" + }, + { + "path": "/v1/service_account/:id/_credential/_status", + "method": "GET" + }, + { + "path": "/v1/service_account/:id/_credential/:cid/_lock", + "method": "GET" + }, + { + "path": "/v1/service_account/:id/_ssh_pubkeys", + "method": "GET" + }, + { + "path": "/v1/service_account/:id/_ssh_pubkeys", + "method": "POST" + }, + { + "path": "/v1/service_account/:id/_ssh_pubkeys/:tag", + "method": "GET" + }, + { + "path": "/v1/service_account/:id/_ssh_pubkeys/:tag", + "method": "DELETE" + }, + { + "path": "/v1/service_account/:id/_unix", + "method": "POST" + }, + { + "path": "/v1/account/:id/_unix/_auth", + "method": "POST" + }, + { + "path": "/v1/account/:id/_ssh_pubkeys", + "method": "GET" + }, + { + "path": "/v1/account/:id/_ssh_pubkeys/:tag", + "method": "GET" + }, + { + "path": "/v1/account/:id/_user_auth_token", + "method": "GET" + }, + { + "path": "/v1/account/:id/_user_auth_token/:token_id", + "method": "DELETE" + }, + { + "path": "/v1/credential/_exchange_intent", + "method": "POST" + }, + { + "path": "/v1/credential/_status", + "method": "POST" + }, + { + "path": "/v1/credential/_update", + "method": "POST" + }, + { + "path": "/v1/credential/_commit", + "method": "POST" + }, + { + "path": "/v1/credential/_cancel", + "method": "POST" + }, + { + "path": "/v1/group", + "method": "GET" + }, + { + "path": "/v1/group", + "method": "POST" + }, + { + "path": "/v1/group/:id", + "method": "GET" + }, + { + "path": "/v1/group/:id", + "method": "DELETE" + }, + { + "path": "/v1/group/:id/_attr/:attr", + "method": "DELETE" + }, + { + "path": "/v1/group/:id/_attr/:attr", + "method": "GET" + }, + { + "path": "/v1/group/:id/_attr/:attr", + "method": "PUT" + }, + { + "path": "/v1/group/:id/_attr/:attr", + "method": "POST" + }, + { + "path": "/v1/group/:id/_unix", + "method": "POST" + }, + { + "path": "/v1/domain", + "method": "GET" + }, + { + "path": "/v1/domain/_attr/:attr", + "method": "GET" + }, + { + "path": "/v1/domain/_attr/:attr", + "method": "PUT" + }, + { + "path": "/v1/domain/_attr/:attr", + "method": "DELETE" + }, + { + "path": "/v1/system", + "method": "GET" + }, + { + "path": "/v1/system/_attr/:attr", + "method": "GET" + }, + { + "path": "/v1/system/_attr/:attr", + "method": "POST" + }, + { + "path": "/v1/system/_attr/:attr", + "method": "DELETE" + }, + { + "path": "/v1/recycle_bin", + "method": "GET" + }, + { + "path": "/v1/recycle_bin/:id", + "method": "GET" + }, + { + "path": "/v1/recycle_bin/:id/_revive", + "method": "POST" + }, + { + "path": "/v1/access_profile", + "method": "GET" + }, + { + "path": "/v1/access_profile/:id", + "method": "GET" + }, + { + "path": "/v1/access_profile/:id/_attr/:attr", + "method": "GET" + } + ] + "#; + // ,{ + // "path": "/v1/routemap", + // "method": "GET" + // } + let routelist: Vec = serde_json::from_str(routemap).unwrap(); + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(); + for route in routelist { + // println!("{:?}", route); + let path: String = route.get("path").unwrap().to_string(); + let method: String = route.get("method").unwrap().to_string(); + let method = method.replace('"', ""); + let method = method.as_str(); + println!("'{method}'"); + let method = match method { + "GET" => reqwest::Method::GET, + "POST" => reqwest::Method::POST, + "DELETE" => reqwest::Method::DELETE, + "PATCH" => reqwest::Method::PATCH, + "PUT" => reqwest::Method::PUT, + _ => todo!("{}", method), + }; + if path.contains(':') { + println!("Can't do this because it has an attribute: {}", path); + continue; + } + let url = format!("{}{}", rsclient.get_url(), path.replace('"', "")); + + println!("#### {:?} {} {}", method, path, url); + + let res = match client + .request(method, &url) + // .version(http::Version::HTTP_11) + .send() + .await + { + Ok(val) => val, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", url, error); + } + }; + if res.status() == 404 { + panic!("Failed to query {:?} : {:#?}", url, res); + } + } +} diff --git a/server/testkit/tests/scim_test.rs b/server/testkit/tests/scim_test.rs index ac4e09888..01034ff89 100644 --- a/server/testkit/tests/scim_test.rs +++ b/server/testkit/tests/scim_test.rs @@ -2,6 +2,7 @@ use compact_jwt::JwsUnverified; use kanidm_client::KanidmClient; use kanidm_proto::internal::ScimSyncToken; use kanidmd_testkit::ADMIN_TEST_PASSWORD; +use reqwest::header::HeaderValue; use std::str::FromStr; use url::Url; @@ -88,3 +89,37 @@ async fn test_sync_account_lifecycle(rsclient: KanidmClient) { .await .expect("Failed to destroy token"); } + +#[kanidmd_testkit::test] +async fn test_scim_sync_get(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {:?}", rsclient.get_token().await)).unwrap(), + ); + + let client = reqwest::Client::builder() + .danger_accept_invalid_certs(true) + .default_headers(headers) + .build() + .unwrap(); + // here we test the /ui/ endpoint which should have the headers + let response = match client.get(format!("{}/scim/v1/Sync", addr)).send().await { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + // assert_eq!(response.status(), 200); + + // eprintln!( + // "csp headers: {:#?}", + // response.headers().get("content-security-policy") + // ); + // assert_ne!(response.headers().get("content-security-policy"), None); + // eprintln!("{}", response.text().await.unwrap()); +} diff --git a/server/testkit/tests/self.rs b/server/testkit/tests/self.rs new file mode 100644 index 000000000..ad92b8281 --- /dev/null +++ b/server/testkit/tests/self.rs @@ -0,0 +1,51 @@ +use kanidm_client::KanidmClient; + +/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better... +#[kanidmd_testkit::test] +async fn test_v1_self_applinks(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + let response = match client + .get(format!("{}/v1/self/_applinks", &addr)) + .send() + .await + { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 401); + + let body = response.text().await.unwrap(); + eprintln!("{}", body); +} + +/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better... +#[kanidmd_testkit::test] +async fn test_v1_self_whoami_uat(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + let response = match client.get(format!("{}/v1/self/_uat", &addr)).send().await { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 401); + + let body = response.text().await.unwrap(); + eprintln!("{}", body); +} diff --git a/server/testkit/tests/service_account.rs b/server/testkit/tests/service_account.rs new file mode 100644 index 000000000..fedbc9888 --- /dev/null +++ b/server/testkit/tests/service_account.rs @@ -0,0 +1,30 @@ +use kanidm_client::KanidmClient; + +/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better... +#[kanidmd_testkit::test] +async fn test_v1_service_account_id_attr_attr_delete(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + // let post_body = serde_json::json!({"filter": "self"}).to_string(); + + let response = match client + .delete(format!("{}/v1/service_account/admin/_attr/email", &addr)) + .send() + .await + { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 401); + + let body = response.text().await.unwrap(); + eprintln!("{}", body); +} diff --git a/server/testkit/tests/system.rs b/server/testkit/tests/system.rs new file mode 100644 index 000000000..b2328addd --- /dev/null +++ b/server/testkit/tests/system.rs @@ -0,0 +1,29 @@ +use kanidm_client::KanidmClient; + +/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better... +#[kanidmd_testkit::test] +async fn test_v1_system_post_attr(rsclient: KanidmClient) { + // We need to do manual reqwests here. + let addr = rsclient.get_url(); + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + let response = match client + .post(format!("{}/v1/system/_attr/domain_name", &addr)) + .json(&serde_json::json!({"filter": "self"})) + .send() + .await + { + Ok(value) => value, + Err(error) => { + panic!("Failed to query {:?} : {:#?}", addr, error); + } + }; + eprintln!("response: {:#?}", response); + assert_eq!(response.status(), 422); + + let body = response.text().await.unwrap(); + eprintln!("{}", body); +} diff --git a/tools/iam_migrations/freeipa/src/main.rs b/tools/iam_migrations/freeipa/src/main.rs index 0ee25706f..cd929167e 100644 --- a/tools/iam_migrations/freeipa/src/main.rs +++ b/tools/iam_migrations/freeipa/src/main.rs @@ -188,30 +188,35 @@ async fn driver_main(opt: Opt) { } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::terminate(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { break } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::alarm(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::hangup(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined1(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined2(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore @@ -685,6 +690,7 @@ fn ipa_to_scim_entry( debug!("{:#?}", sync_entry); // check the sync_entry state? + #[allow(clippy::unimplemented)] if sync_entry.state != LdapSyncStateValue::Add { unimplemented!(); } @@ -732,7 +738,7 @@ fn ipa_to_scim_entry( error!("Missing required attribute cn"); })?; - let gidnumber = if let Some(number) = entry_config.map_gidnumber.clone() { + let gidnumber = if let Some(number) = entry_config.map_gidnumber { Some(number) } else { entry diff --git a/tools/iam_migrations/ldap/src/main.rs b/tools/iam_migrations/ldap/src/main.rs index b2f3ce626..da7f9f9da 100644 --- a/tools/iam_migrations/ldap/src/main.rs +++ b/tools/iam_migrations/ldap/src/main.rs @@ -178,30 +178,35 @@ async fn driver_main(opt: Opt) { } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::terminate(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { break } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::alarm(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::hangup(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined1(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined2(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore @@ -372,7 +377,7 @@ async fn run_sync( } }; - let entries = match process_ldap_sync_result(entries, &sync_config).await { + let entries = match process_ldap_sync_result(entries, sync_config).await { Ok(ssr) => ssr, Err(()) => { error!("Failed to process IPA entries to SCIM"); @@ -454,6 +459,7 @@ fn ldap_to_scim_entry( debug!("{:#?}", sync_entry); // check the sync_entry state? + #[allow(clippy::unimplemented)] if sync_entry.state != LdapSyncStateValue::Add { unimplemented!(); } @@ -505,7 +511,7 @@ fn ldap_to_scim_entry( ); })?; - let gidnumber = if let Some(number) = entry_config.map_gidnumber.clone() { + let gidnumber = if let Some(number) = entry_config.map_gidnumber { Some(number) } else { entry diff --git a/unix_integration/pam_kanidm/src/pam/module.rs b/unix_integration/pam_kanidm/src/pam/module.rs index 5ea8fbfd0..c8363e0cf 100755 --- a/unix_integration/pam_kanidm/src/pam/module.rs +++ b/unix_integration/pam_kanidm/src/pam/module.rs @@ -67,6 +67,8 @@ pub unsafe extern "C" fn cleanup(_: *const PamHandle, c_data: *mut PamDataT, pub type PamResult = Result; +/// # Safety +/// /// Type-level mapping for safely retrieving values with `get_item`. /// /// See `pam_get_item` in @@ -82,6 +84,8 @@ pub trait PamItem { } impl PamHandle { + /// # Safety + /// /// Gets some value, identified by `key`, that has been set by the module /// previously. /// diff --git a/unix_integration/src/daemon.rs b/unix_integration/src/daemon.rs index 4fbee2520..78f3d0db2 100644 --- a/unix_integration/src/daemon.rs +++ b/unix_integration/src/daemon.rs @@ -376,13 +376,13 @@ async fn process_etc_passwd_group(cachelayer: &CacheLayer) -> Result<(), Box ExitCode { std::env::set_var("RUST_LOG", "debug"); } + #[allow(clippy::expect_used)] tracing_forest::worker_task() .set_global(true) // Fall back to stderr @@ -699,7 +700,7 @@ async fn main() -> ExitCode { let _ = unsafe { umask(before) }; // Pre-process /etc/passwd and /etc/group for nxset - if let Err(_) = process_etc_passwd_group(&cachelayer).await { + if process_etc_passwd_group(&cachelayer).await.is_err() { error!("Failed to process system id providers"); return ExitCode::FAILURE } @@ -800,7 +801,7 @@ async fn main() -> ExitCode { break; } _ = inotify_rx.recv() => { - if let Err(_) = process_etc_passwd_group(&inotify_cachelayer).await { + if process_etc_passwd_group(&inotify_cachelayer).await.is_err() { error!("Failed to process system id providers"); } } @@ -860,30 +861,35 @@ async fn main() -> ExitCode { } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::terminate(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { break } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::alarm(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::hangup(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined1(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined2(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore diff --git a/unix_integration/src/selinux_util.rs b/unix_integration/src/selinux_util.rs index 9aa5e7a76..eb561553a 100644 --- a/unix_integration/src/selinux_util.rs +++ b/unix_integration/src/selinux_util.rs @@ -30,7 +30,7 @@ pub fn do_setfscreatecon_for_path( ) -> Result<(), String> { match labeler.look_up(&CString::new(path_raw.to_owned()).unwrap(), 0) { Ok(context) => { - if let Err(_) = context.set_for_new_file_system_objects(true) { + if context.set_for_new_file_system_objects(true).is_err() { return Err("Failed setting creation context home directory path".to_string()); } Ok(()) diff --git a/unix_integration/src/tasks_daemon.rs b/unix_integration/src/tasks_daemon.rs index b596cb5c4..19a7689a5 100644 --- a/unix_integration/src/tasks_daemon.rs +++ b/unix_integration/src/tasks_daemon.rs @@ -292,6 +292,7 @@ async fn main() -> ExitCode { let ceuid = get_effective_uid(); let cegid = get_effective_gid(); + #[allow(clippy::expect_used)] tracing_forest::worker_task() .set_global(true) // Fall back to stderr @@ -376,30 +377,36 @@ async fn main() -> ExitCode { } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::terminate(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { break } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::alarm(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::hangup(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined1(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore } + Some(()) = async move { let sigterm = tokio::signal::unix::SignalKind::user_defined2(); + #[allow(clippy::unwrap_used)] tokio::signal::unix::signal(sigterm).unwrap().recv().await } => { // Ignore diff --git a/unix_integration/src/unix_config.rs b/unix_integration/src/unix_config.rs index 144101aa3..a1530ed8e 100644 --- a/unix_integration/src/unix_config.rs +++ b/unix_integration/src/unix_config.rs @@ -6,6 +6,7 @@ use std::path::Path; #[cfg(all(target_family = "unix", feature = "selinux"))] use crate::selinux_util; +use crate::unix_passwd::UnixIntegrationError; use serde::Deserialize; @@ -191,7 +192,7 @@ impl KanidmUnixdConfig { pub fn read_options_from_optional_config + std::fmt::Debug>( self, config_path: P, - ) -> Result { + ) -> Result { debug!("Attempting to load configuration from {:#?}", &config_path); let mut f = match File::open(&config_path) { Ok(f) => { @@ -224,11 +225,15 @@ impl KanidmUnixdConfig { }; let mut contents = String::new(); - f.read_to_string(&mut contents) - .map_err(|e| eprintln!("{:?}", e))?; + f.read_to_string(&mut contents).map_err(|e| { + error!("{:?}", e); + UnixIntegrationError + })?; - let config: ConfigInt = - toml::from_str(contents.as_str()).map_err(|e| eprintln!("{:?}", e))?; + let config: ConfigInt = toml::from_str(contents.as_str()).map_err(|e| { + error!("{:?}", e); + UnixIntegrationError + })?; // Now map the values into our config. Ok(KanidmUnixdConfig { diff --git a/unix_integration/src/unix_passwd.rs b/unix_integration/src/unix_passwd.rs index 098f938bf..855cd0617 100644 --- a/unix_integration/src/unix_passwd.rs +++ b/unix_integration/src/unix_passwd.rs @@ -16,15 +16,15 @@ pub struct EtcUser { pub shell: String, } -pub fn parse_etc_passwd(bytes: &[u8]) -> Result, ()> { +pub fn parse_etc_passwd(bytes: &[u8]) -> Result, UnixIntegrationError> { use csv::ReaderBuilder; let mut rdr = ReaderBuilder::new() .has_headers(false) .delimiter(b':') .from_reader(bytes); rdr.deserialize() - .map(|result| result.map_err(|_e| ())) - .collect::, ()>>() + .map(|result| result.map_err(|_e| UnixIntegrationError)) + .collect::, UnixIntegrationError>>() } fn members<'de, D>(deserializer: D) -> Result, D::Error> @@ -60,15 +60,18 @@ pub struct EtcGroup { pub members: Vec, } -pub fn parse_etc_group(bytes: &[u8]) -> Result, ()> { +#[derive(Debug)] +pub struct UnixIntegrationError; + +pub fn parse_etc_group(bytes: &[u8]) -> Result, UnixIntegrationError> { use csv::ReaderBuilder; let mut rdr = ReaderBuilder::new() .has_headers(false) .delimiter(b':') .from_reader(bytes); rdr.deserialize() - .map(|result| result.map_err(|_e| ())) - .collect::, ()>>() + .map(|result| result.map_err(|_e| UnixIntegrationError)) + .collect::, UnixIntegrationError>>() } #[cfg(test)] diff --git a/unix_integration/tests/cache_layer_test.rs b/unix_integration/tests/cache_layer_test.rs index b61fac69e..915fa8db2 100644 --- a/unix_integration/tests/cache_layer_test.rs +++ b/unix_integration/tests/cache_layer_test.rs @@ -15,6 +15,7 @@ use kanidm_unix_common::unix_config::TpmPolicy; use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole}; use kanidmd_core::create_server_core; use tokio::task; +use tracing::log::debug; static PORT_ALLOC: AtomicU16 = AtomicU16::new(28080); const ADMIN_TEST_USER: &str = "admin"; @@ -72,7 +73,7 @@ async fn setup_test(fix_fn: Fixture) -> (CacheLayer, KanidmClient) { create_server_core(config, false) .await .expect("failed to start server core"); - // We have to yield now to guarantee that the tide elements are setup. + // We have to yield now to guarantee that the elements are setup. task::yield_now().await; // Setup the client, and the address we selected. @@ -126,6 +127,7 @@ async fn test_fixture(rsclient: KanidmClient) { let res = rsclient .auth_simple_password("admin", ADMIN_TEST_PASSWORD) .await; + debug!("auth_simple_password res: {:?}", res); assert!(res.is_ok()); // Not recommended in production! rsclient @@ -664,10 +666,13 @@ async fn test_cache_nxcache() { assert!(gt.is_none()); // Should all now be nxed - assert!(cachelayer - .check_nxcache(&Id::Name("oracle".to_string())) - .await - .is_some()); + assert!( + cachelayer + .check_nxcache(&Id::Name("oracle".to_string())) + .await + .is_some(), + "'oracle' Wasn't in the nxcache!" + ); assert!(cachelayer.check_nxcache(&Id::Gid(2000)).await.is_some()); assert!(cachelayer .check_nxcache(&Id::Name("oracle_group".to_string()))