diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74f9c44c..0202e681 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -120,37 +120,37 @@ jobs: # First test all features together, afterwards test them separately. - name: "test features: sqlite,mysql,postgresql,enable_mimalloc,query_logger" id: test_sqlite_mysql_postgresql_mimalloc_logger - if: $${{ always() }} + if: ${{ !cancelled() }} run: | cargo test --features sqlite,mysql,postgresql,enable_mimalloc,query_logger - name: "test features: sqlite,mysql,postgresql,enable_mimalloc" id: test_sqlite_mysql_postgresql_mimalloc - if: $${{ always() }} + if: ${{ !cancelled() }} run: | cargo test --features sqlite,mysql,postgresql,enable_mimalloc - name: "test features: sqlite,mysql,postgresql" id: test_sqlite_mysql_postgresql - if: $${{ always() }} + if: ${{ !cancelled() }} run: | cargo test --features sqlite,mysql,postgresql - name: "test features: sqlite" id: test_sqlite - if: $${{ always() }} + if: ${{ !cancelled() }} run: | cargo test --features sqlite - name: "test features: mysql" id: test_mysql - if: $${{ always() }} + if: ${{ !cancelled() }} run: | cargo test --features mysql - name: "test features: postgresql" id: test_postgresql - if: $${{ always() }} + if: ${{ !cancelled() }} run: | cargo test --features postgresql # End Run cargo tests @@ -159,7 +159,7 @@ jobs: # Run cargo clippy, and fail on warnings - name: "clippy features: sqlite,mysql,postgresql,enable_mimalloc" id: clippy - if: ${{ always() && matrix.channel == 'rust-toolchain' }} + if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }} run: | cargo clippy --features sqlite,mysql,postgresql,enable_mimalloc -- -D warnings # End Run cargo clippy @@ -168,7 +168,7 @@ jobs: # Run cargo fmt (Only run on rust-toolchain defined version) - name: "check formatting" id: formatting - if: ${{ always() && matrix.channel == 'rust-toolchain' }} + if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }} run: | cargo fmt --all -- --check # End Run cargo fmt diff --git a/Cargo.lock b/Cargo.lock index bc02b5cd..f8475f37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -371,9 +371,9 @@ checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "blake2" @@ -419,9 +419,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.1" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -489,9 +489,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "cc" -version = "1.2.7" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ "shlex", ] @@ -517,9 +517,9 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6" +checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f" dependencies = [ "chrono", "chrono-tz-build", @@ -702,9 +702,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "data-url" @@ -1152,9 +1152,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", @@ -1342,7 +1342,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.9", + "thiserror 2.0.11", "walkdir", ] @@ -1586,7 +1586,7 @@ dependencies = [ "http 1.2.0", "hyper 1.5.2", "hyper-util", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-pki-types", "tokio", "tokio-rustls 0.26.1", @@ -1798,9 +1798,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -1827,19 +1827,19 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1867,9 +1867,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -2014,9 +2014,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" dependencies = [ "value-bag", ] @@ -2118,9 +2118,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] @@ -2336,9 +2336,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" @@ -2469,7 +2469,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.9", + "thiserror 2.0.11", "ucd-trie", ] @@ -2641,9 +2641,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -2765,9 +2765,9 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.2.0" +version = "11.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +checksum = "c6928fa44c097620b706542d428957635951bade7143269085389d42c8a4927e" dependencies = [ "bitflags", ] @@ -3077,9 +3077,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", @@ -3102,9 +3102,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "once_cell", "rustls-pki-types", @@ -3244,9 +3244,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" @@ -3280,9 +3280,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", @@ -3369,13 +3369,13 @@ dependencies = [ [[package]] name = "simple_asn1" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 1.0.69", + "thiserror 2.0.11", "time", ] @@ -3476,9 +3476,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.95" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -3563,11 +3563,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.11", ] [[package]] @@ -3583,9 +3583,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -3671,9 +3671,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -3689,9 +3689,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -3724,7 +3724,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.20", + "rustls 0.23.21", "tokio", ] @@ -3969,9 +3969,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" [[package]] name = "unicode-xid" @@ -4017,18 +4017,18 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ "getrandom", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" @@ -4141,20 +4141,21 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -4166,9 +4167,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -4179,9 +4180,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4189,9 +4190,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -4202,9 +4203,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -4221,9 +4225,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -4515,9 +4519,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.22" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 9084c842..e3755e26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ syslog = "7.0.0" macros = { path = "./macros" } # Logging -log = "0.4.22" +log = "0.4.25" fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] } tracing = { version = "0.1.41", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work @@ -71,11 +71,11 @@ dashmap = "6.1.0" # Async futures futures = "0.3.31" -tokio = { version = "1.42.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio = { version = "1.43.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } # A generic serialization/deserialization framework serde = { version = "1.0.217", features = ["derive"] } -serde_json = "1.0.135" +serde_json = "1.0.137" # A safe, extensible ORM and Query builder diesel = { version = "2.2.6", features = ["chrono", "r2d2", "numeric"] } @@ -93,18 +93,18 @@ rand = { version = "0.8.5", features = ["small_rng"] } ring = "0.17.8" # UUID generation -uuid = { version = "1.11.0", features = ["v4"] } +uuid = { version = "1.12.1", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.39", features = ["clock", "serde"], default-features = false } -chrono-tz = "0.10.0" +chrono-tz = "0.10.1" time = "0.3.37" # Job scheduler job_scheduler_ng = "2.0.5" # Data encoding library Hex/Base32/Base64 -data-encoding = "2.6.0" +data-encoding = "2.7.0" # JWT library jsonwebtoken = "9.3.0" @@ -157,7 +157,7 @@ paste = "1.0.15" governor = "0.8.0" # Check client versions for specific features. -semver = "1.0.24" +semver = "1.0.25" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 184accfc..3beea5d8 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -10,4 +10,4 @@ proc-macro = true [dependencies] quote = "1.0.38" -syn = "2.0.94" +syn = "2.0.96" diff --git a/src/api/admin.rs b/src/api/admin.rs index 31159816..a5a12e8a 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -171,7 +171,7 @@ struct LoginForm { redirect: Option, } -#[post("/", data = "")] +#[post("/", format = "application/x-www-form-urlencoded", data = "")] fn post_admin_login( data: Form, cookies: &CookieJar<'_>, @@ -289,7 +289,7 @@ async fn get_user_or_404(user_id: &UserId, conn: &mut DbConn) -> ApiResult } } -#[post("/invite", data = "")] +#[post("/invite", format = "application/json", data = "")] async fn invite_user(data: Json, _token: AdminToken, mut conn: DbConn) -> JsonResult { let data: InviteData = data.into_inner(); if User::find_by_mail(&data.email, &mut conn).await.is_some() { @@ -315,7 +315,7 @@ async fn invite_user(data: Json, _token: AdminToken, mut conn: DbCon Ok(Json(user.to_json(&mut conn).await)) } -#[post("/test/smtp", data = "")] +#[post("/test/smtp", format = "application/json", data = "")] async fn test_smtp(data: Json, _token: AdminToken) -> EmptyResult { let data: InviteData = data.into_inner(); @@ -393,7 +393,7 @@ async fn get_user_json(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> Ok(Json(usr)) } -#[post("/users//delete")] +#[post("/users//delete", format = "application/json")] async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult { let user = get_user_or_404(&user_id, &mut conn).await?; @@ -417,7 +417,7 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em res } -#[post("/users//deauth")] +#[post("/users//deauth", format = "application/json")] async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&user_id, &mut conn).await?; @@ -438,7 +438,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: user.save(&mut conn).await } -#[post("/users//disable")] +#[post("/users//disable", format = "application/json")] async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&user_id, &mut conn).await?; Device::delete_all_by_user(&user.uuid, &mut conn).await?; @@ -452,7 +452,7 @@ async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: save_result } -#[post("/users//enable")] +#[post("/users//enable", format = "application/json")] async fn enable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { let mut user = get_user_or_404(&user_id, &mut conn).await?; user.enabled = true; @@ -460,7 +460,7 @@ async fn enable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> E user.save(&mut conn).await } -#[post("/users//remove-2fa")] +#[post("/users//remove-2fa", format = "application/json")] async fn remove_2fa(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult { let mut user = get_user_or_404(&user_id, &mut conn).await?; TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; @@ -469,7 +469,7 @@ async fn remove_2fa(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Emp user.save(&mut conn).await } -#[post("/users//invite/resend")] +#[post("/users//invite/resend", format = "application/json")] async fn resend_user_invite(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { if let Some(user) = User::find_by_uuid(&user_id, &mut conn).await { //TODO: replace this with user.status check when it will be available (PR#3397) @@ -496,7 +496,7 @@ struct MembershipTypeData { org_uuid: OrganizationId, } -#[post("/users/org_type", data = "")] +#[post("/users/org_type", format = "application/json", data = "")] async fn update_membership_type(data: Json, token: AdminToken, mut conn: DbConn) -> EmptyResult { let data: MembershipTypeData = data.into_inner(); @@ -550,7 +550,7 @@ async fn update_membership_type(data: Json, token: AdminToke member_to_edit.save(&mut conn).await } -#[post("/users/update_revision")] +#[post("/users/update_revision", format = "application/json")] async fn update_revision_users(_token: AdminToken, mut conn: DbConn) -> EmptyResult { User::update_all_revisions(&mut conn).await } @@ -575,7 +575,7 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu Ok(Html(text)) } -#[post("/organizations//delete")] +#[post("/organizations//delete", format = "application/json")] async fn delete_organization(org_id: OrganizationId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { let org = Organization::find_by_uuid(&org_id, &mut conn).await.map_res("Organization doesn't exist")?; org.delete(&mut conn).await @@ -733,7 +733,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) Ok(Html(text)) } -#[get("/diagnostics/config")] +#[get("/diagnostics/config", format = "application/json")] fn get_diagnostics_config(_token: AdminToken) -> Json { let support_json = CONFIG.get_support_json(); Json(support_json) @@ -744,16 +744,16 @@ fn get_diagnostics_http(code: u16, _token: AdminToken) -> EmptyResult { err_code!(format!("Testing error {code} response"), code); } -#[post("/config", data = "")] +#[post("/config", format = "application/json", data = "")] fn post_config(data: Json, _token: AdminToken) -> EmptyResult { let data: ConfigBuilder = data.into_inner(); - if let Err(e) = CONFIG.update_config(data) { + if let Err(e) = CONFIG.update_config(data, true) { err!(format!("Unable to save config: {e:?}")) } Ok(()) } -#[post("/config/delete")] +#[post("/config/delete", format = "application/json")] fn delete_config(_token: AdminToken) -> EmptyResult { if let Err(e) = CONFIG.delete_user_config() { err!(format!("Unable to delete config: {e:?}")) @@ -761,7 +761,7 @@ fn delete_config(_token: AdminToken) -> EmptyResult { Ok(()) } -#[post("/config/backup_db")] +#[post("/config/backup_db", format = "application/json")] async fn backup_db(_token: AdminToken, mut conn: DbConn) -> ApiResult { if *CAN_BACKUP { match backup_database(&mut conn).await { diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 11ae66c7..012f46cc 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -34,9 +34,13 @@ struct EventRange { async fn get_org_events( org_id: OrganizationId, data: EventRange, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } + // Return an empty vec when we org events are disabled. // This prevents client errors let events_json: Vec = if !CONFIG.org_events_enabled() { @@ -100,9 +104,12 @@ async fn get_user_events( org_id: OrganizationId, member_id: MembershipId, data: EventRange, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } // Return an empty vec when we org events are disabled. // This prevents client errors let events_json: Vec = if !CONFIG.org_events_enabled() { diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 61868c0b..172bca42 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -210,8 +210,8 @@ fn config() -> Json { // This means they expect a version that closely matches the Bitwarden server version // We should make sure that we keep this updated when we support the new server features // Version history: - // - Individual cipher key encryption: 2023.9.1 - "version": "2024.2.0", + // - Individual cipher key encryption: 2024.2.0 + "version": "2025.1.0", "gitHash": option_env!("GIT_REV"), "server": { "name": "Vaultwarden", diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index bd4c1a77..c610f5b5 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -10,7 +10,10 @@ use crate::{ core::{log_event, two_factor, CipherSyncData, CipherSyncType}, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, }, - auth::{decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, + auth::{ + decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, + OwnerHeaders, + }, db::{models::*, DbConn}, mail, util::{convert_json_key_lcase_first, NumberOrString}, @@ -213,6 +216,9 @@ async fn delete_organization( headers: OwnerHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: PasswordOrOtpData = data.into_inner(); data.validate(&headers.user, true, &mut conn).await?; @@ -261,7 +267,10 @@ async fn leave_organization(org_id: OrganizationId, headers: Headers, mut conn: } #[get("/organizations/")] -async fn get_organization(org_id: OrganizationId, _headers: OwnerHeaders, mut conn: DbConn) -> JsonResult { +async fn get_organization(org_id: OrganizationId, headers: OwnerHeaders, mut conn: DbConn) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } match Organization::find_by_uuid(&org_id, &mut conn).await { Some(organization) => Ok(Json(organization.to_json())), None => err!("Can't find organization details"), @@ -285,14 +294,18 @@ async fn post_organization( data: Json, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } + let data: OrganizationUpdateData = data.into_inner(); let Some(mut org) = Organization::find_by_uuid(&org_id, &mut conn).await else { - err!("Can't find organization details") + err!("Organization not found") }; org.name = data.name; - org.billing_email = data.billing_email; + org.billing_email = data.billing_email.to_lowercase(); org.save(&mut conn).await?; @@ -325,12 +338,15 @@ async fn get_user_collections(headers: Headers, mut conn: DbConn) -> Json } #[get("/organizations//collections")] -async fn get_org_collections(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { - Json(json!({ +async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } + Ok(Json(json!({ "data": _get_org_collections(&org_id, &mut conn).await, "object": "list", "continuationToken": null, - })) + }))) } #[get("/organizations//collections/details")] @@ -339,6 +355,9 @@ async fn get_org_collections_details( headers: ManagerHeadersLoose, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let mut data = Vec::new(); let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await else { @@ -413,6 +432,9 @@ async fn post_organization_collections( data: Json, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let data: NewCollectionData = data.into_inner(); let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { @@ -485,6 +507,9 @@ async fn post_organization_collection_update( data: Json, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: NewCollectionData = data.into_inner(); if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() { @@ -545,9 +570,12 @@ async fn delete_organization_collection_member( org_id: OrganizationId, col_id: CollectionId, member_id: MembershipId, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else { err!("Collection not found", "Collection does not exist or does not belong to this organization") }; @@ -638,6 +666,9 @@ async fn bulk_delete_organization_collections( data: Json, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let data: BulkCollectionIds = data.into_inner(); let collections = data.ids; @@ -657,6 +688,9 @@ async fn get_org_collection_detail( headers: ManagerHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } match Collection::find_by_uuid_and_user(&col_id, headers.user.uuid.clone(), &mut conn).await { None => err!("Collection not found"), Some(collection) => { @@ -718,9 +752,12 @@ async fn get_org_collection_detail( async fn get_collection_users( org_id: OrganizationId, col_id: CollectionId, - _headers: ManagerHeaders, + headers: ManagerHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } // Get org and collection, check that collection is from org let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else { err!("Collection not found in Organization") @@ -744,9 +781,12 @@ async fn put_collection_users( org_id: OrganizationId, col_id: CollectionId, data: Json>, - _headers: ManagerHeaders, + headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } // Get org and collection, check that collection is from org if Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await.is_none() { err!("Collection not found in Organization") @@ -778,10 +818,9 @@ struct OrgIdData { } #[get("/ciphers/organization-details?")] -async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> JsonResult { - if Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &mut conn).await.is_none() - { - err_code!("Resource not found.", rocket::http::Status::NotFound.code); +async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: DbConn) -> JsonResult { + if data.organization_id != headers.org_id { + err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code); } Ok(Json(json!({ @@ -814,9 +853,12 @@ struct GetOrgUserData { async fn get_members( data: GetOrgUserData, org_id: OrganizationId, - _headers: ManagerHeadersLoose, + headers: ManagerHeadersLoose, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let mut users_json = Vec::new(); for u in Membership::find_by_org(&org_id, &mut conn).await { users_json.push( @@ -829,20 +871,23 @@ async fn get_members( ); } - Json(json!({ + Ok(Json(json!({ "data": users_json, "object": "list", "continuationToken": null, - })) + }))) } #[post("/organizations//keys", data = "")] async fn post_org_keys( org_id: OrganizationId, data: Json, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: OrgKeyData = data.into_inner(); let mut org = match Organization::find_by_uuid(&org_id, &mut conn).await { @@ -905,6 +950,9 @@ async fn send_invite( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let mut data: InviteData = data.into_inner(); // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission @@ -1045,7 +1093,10 @@ async fn bulk_reinvite_members( data: Json, headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: BulkMembershipIds = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1064,11 +1115,11 @@ async fn bulk_reinvite_members( )) } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } #[post("/organizations//users//reinvite")] @@ -1237,7 +1288,10 @@ async fn bulk_confirm_invite( headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1263,11 +1317,11 @@ async fn bulk_confirm_invite( None => error!("No keys to confirm"), } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } #[post("/organizations//users//confirm", data = "")] @@ -1364,19 +1418,22 @@ async fn _confirm_invite( #[get("/organizations//users/mini-details", rank = 1)] async fn get_org_user_mini_details( org_id: OrganizationId, - _headers: ManagerHeadersLoose, + headers: ManagerHeadersLoose, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let mut members_json = Vec::new(); for m in Membership::find_by_org(&org_id, &mut conn).await { members_json.push(m.to_json_mini_details(&mut conn).await); } - Json(json!({ + Ok(Json(json!({ "data": members_json, "object": "list", "continuationToken": null, - })) + }))) } #[get("/organizations//users/?", rank = 2)] @@ -1384,9 +1441,12 @@ async fn get_user( org_id: OrganizationId, member_id: MembershipId, data: GetOrgUserData, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(user) = Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await else { err!("The specified user isn't a member of the organization") }; @@ -1430,6 +1490,9 @@ async fn edit_member( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let mut data: EditUserData = data.into_inner(); // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission @@ -1551,7 +1614,10 @@ async fn bulk_delete_member( headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: BulkMembershipIds = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1570,11 +1636,11 @@ async fn bulk_delete_member( )) } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } #[delete("/organizations//users/")] @@ -1644,9 +1710,12 @@ async fn _delete_member( async fn bulk_public_keys( org_id: OrganizationId, data: Json, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: BulkMembershipIds = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1670,11 +1739,11 @@ async fn bulk_public_keys( } } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } use super::ciphers::update_cipher_from_data; @@ -1819,15 +1888,18 @@ async fn post_bulk_collections(data: Json, headers: Headers } #[get("/organizations//policies")] -async fn list_policies(org_id: OrganizationId, _headers: AdminHeaders, mut conn: DbConn) -> Json { +async fn list_policies(org_id: OrganizationId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await; let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); - Json(json!({ + Ok(Json(json!({ "data": policies_json, "object": "list", "continuationToken": null - })) + }))) } #[get("/organizations//policies/token?")] @@ -1855,7 +1927,10 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbCo } #[get("/organizations//policies/")] -async fn get_policy(org_id: OrganizationId, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, mut conn: DbConn) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else { err!("Invalid or unsupported policy type") }; @@ -1884,6 +1959,9 @@ async fn put_policy( headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: PolicyData = data.into_inner(); let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else { @@ -2217,7 +2295,7 @@ async fn bulk_deactivate_members( data: Json, headers: AdminHeaders, conn: DbConn, -) -> Json { +) -> JsonResult { bulk_revoke_members(org_id, data, headers, conn).await } @@ -2237,7 +2315,10 @@ async fn bulk_revoke_members( data: Json, headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data = data.into_inner(); let mut bulk_response = Vec::new(); @@ -2261,11 +2342,11 @@ async fn bulk_revoke_members( None => error!("No users to revoke"), } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } async fn _revoke_member( @@ -2326,7 +2407,7 @@ async fn bulk_activate_members( data: Json, headers: AdminHeaders, conn: DbConn, -) -> Json { +) -> JsonResult { bulk_restore_members(org_id, data, headers, conn).await } @@ -2346,7 +2427,10 @@ async fn bulk_restore_members( data: Json, headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data = data.into_inner(); let mut bulk_response = Vec::new(); @@ -2365,11 +2449,11 @@ async fn bulk_restore_members( )); } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } async fn _restore_member( @@ -2426,7 +2510,10 @@ async fn _restore_member( } #[get("/organizations//groups")] -async fn get_groups(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { +async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let groups: Vec = if CONFIG.org_groups_enabled() { // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::() let groups = Group::find_by_organization(&org_id, &mut conn).await; @@ -2614,9 +2701,12 @@ async fn add_update_group( async fn get_group_details( org_id: OrganizationId, group_id: GroupId, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2696,7 +2786,10 @@ async fn bulk_delete_groups( } #[get("/organizations//groups/", rank = 2)] -async fn get_group(org_id: OrganizationId, group_id: GroupId, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2712,9 +2805,12 @@ async fn get_group(org_id: OrganizationId, group_id: GroupId, _headers: AdminHea async fn get_group_members( org_id: OrganizationId, group_id: GroupId, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2774,9 +2870,12 @@ async fn put_group_members( async fn get_user_groups( org_id: OrganizationId, member_id: MembershipId, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2816,6 +2915,9 @@ async fn put_user_groups( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2865,6 +2967,9 @@ async fn delete_group_member( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2910,7 +3015,14 @@ struct OrganizationUserResetPasswordRequest { // But the clients do not seem to use this at all // Just add it here in case they will #[get("/organizations//public-key")] -async fn get_organization_public_key(org_id: OrganizationId, _headers: Headers, mut conn: DbConn) -> JsonResult { +async fn get_organization_public_key( + org_id: OrganizationId, + headers: OrgMemberHeaders, + mut conn: DbConn, +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Organization not found") }; @@ -2924,7 +3036,7 @@ async fn get_organization_public_key(org_id: OrganizationId, _headers: Headers, // Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients // https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468 #[get("/organizations//keys")] -async fn get_organization_keys(org_id: OrganizationId, headers: Headers, conn: DbConn) -> JsonResult { +async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { get_organization_public_key(org_id, headers, conn).await } @@ -2937,6 +3049,9 @@ async fn put_reset_password( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Required organization not found") }; @@ -2993,6 +3108,9 @@ async fn get_reset_password_details( headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Required organization not found") }; @@ -3114,7 +3232,10 @@ async fn get_org_export( headers: AdminHeaders, client_version: Option, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } // Since version v2023.1.0 the format of the export is different. // Also, this endpoint was created since v2022.9.0. // Therefore, we will check for any version smaller then v2023.1.0 and return a different response. @@ -3130,7 +3251,7 @@ async fn get_org_export( // Also both main keys here need to be lowercase, else the export will fail. if use_list_response_model { // Backwards compatible pre v2023.1.0 response - Json(json!({ + Ok(Json(json!({ "collections": { "data": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await), "object": "list", @@ -3141,13 +3262,13 @@ async fn get_org_export( "object": "list", "continuationToken": null, } - })) + }))) } else { // v2023.1.0 and newer response - Json(json!({ + Ok(Json(json!({ "collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await), "ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await), - })) + }))) } } diff --git a/src/auth.rs b/src/auth.rs index e1dd71f6..e4827c80 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -557,24 +557,17 @@ impl<'r> FromRequest<'r> for OrgHeaders { // but there are cases where it is a query value. // First check the path, if this is not a valid uuid, try the query values. let url_org_id: Option = { - let mut url_org_id = None; - if let Some(Ok(org_id)) = request.param::<&str>(1) { - if uuid::Uuid::parse_str(org_id).is_ok() { - url_org_id = Some(org_id.to_string().into()); - } + if let Some(Ok(org_id)) = request.param::(1) { + Some(org_id.clone()) + } else if let Some(Ok(org_id)) = request.query_value::("organizationId") { + Some(org_id.clone()) + } else { + None } - - if let Some(Ok(org_id)) = request.query_value::<&str>("organizationId") { - if uuid::Uuid::parse_str(org_id).is_ok() { - url_org_id = Some(org_id.to_string().into()); - } - } - - url_org_id }; match url_org_id { - Some(org_id) => { + Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => { let mut conn = match DbConn::from_request(request).await { Outcome::Success(conn) => conn, _ => err_handler!("Error getting DB"), @@ -619,6 +612,7 @@ pub struct AdminHeaders { pub user: User, pub membership_type: MembershipType, pub ip: ClientIp, + pub org_id: OrganizationId, } #[rocket::async_trait] @@ -634,6 +628,7 @@ impl<'r> FromRequest<'r> for AdminHeaders { user: headers.user, membership_type: headers.membership_type, ip: headers.ip, + org_id: headers.membership.org_uuid, }) } else { err_handler!("You need to be Admin or Owner to call this endpoint") @@ -679,6 +674,7 @@ pub struct ManagerHeaders { pub device: Device, pub user: User, pub ip: ClientIp, + pub org_id: OrganizationId, } #[rocket::async_trait] @@ -707,6 +703,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { device: headers.device, user: headers.user, ip: headers.ip, + org_id: headers.membership.org_uuid, }) } else { err_handler!("You need to be a Manager, Admin or Owner to call this endpoint") @@ -786,6 +783,7 @@ impl ManagerHeaders { device: h.device, user: h.user, ip: h.ip, + org_id: h.membership.org_uuid, }) } } @@ -794,6 +792,7 @@ pub struct OwnerHeaders { pub device: Device, pub user: User, pub ip: ClientIp, + pub org_id: OrganizationId, } #[rocket::async_trait] @@ -807,6 +806,7 @@ impl<'r> FromRequest<'r> for OwnerHeaders { device: headers.device, user: headers.user, ip: headers.ip, + org_id: headers.membership.org_uuid, }) } else { err_handler!("You need to be Owner to call this endpoint") @@ -814,6 +814,30 @@ impl<'r> FromRequest<'r> for OwnerHeaders { } } +pub struct OrgMemberHeaders { + pub host: String, + pub user: User, + pub org_id: OrganizationId, +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for OrgMemberHeaders { + type Error = &'static str; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + let headers = try_outcome!(OrgHeaders::from_request(request).await); + if headers.membership_type >= MembershipType::User { + Outcome::Success(Self { + host: headers.host, + user: headers.user, + org_id: headers.membership.org_uuid, + }) + } else { + err_handler!("You need to be a Member of the Organization to call this endpoint") + } + } +} + // // Client IP address detection // diff --git a/src/config.rs b/src/config.rs index e8536209..da3bbd44 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,10 @@ -use std::env::consts::EXE_SUFFIX; -use std::process::exit; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - RwLock, +use std::{ + env::consts::EXE_SUFFIX, + process::exit, + sync::{ + atomic::{AtomicBool, Ordering}, + RwLock, + }, }; use job_scheduler_ng::Schedule; @@ -12,7 +14,7 @@ use reqwest::Url; use crate::{ db::DbConnType, error::Error, - util::{get_env, get_env_bool, get_web_vault_version, parse_experimental_client_feature_flags}, + util::{get_env, get_env_bool, get_web_vault_version, is_valid_email, parse_experimental_client_feature_flags}, }; static CONFIG_FILE: Lazy = Lazy::new(|| { @@ -114,6 +116,14 @@ macro_rules! make_config { serde_json::from_str(&config_str).map_err(Into::into) } + fn clear_non_editable(&mut self) { + $($( + if !$editable { + self.$name = None; + } + )+)+ + } + /// Merges the values of both builders into a new builder. /// If both have the same element, `other` wins. fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec) -> Self { @@ -677,7 +687,7 @@ make_config! { /// Use Sendmail |> Whether to send mail via the `sendmail` command use_sendmail: bool, true, def, false; /// Sendmail Command |> Which sendmail command to use. The one found in the $PATH is used if not specified. - sendmail_command: String, true, option; + sendmail_command: String, false, option; /// Host smtp_host: String, true, option; /// DEPRECATED smtp_ssl |> DEPRECATED - Please use SMTP_SECURITY @@ -893,12 +903,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { let command = cfg.sendmail_command.clone().unwrap_or_else(|| format!("sendmail{EXE_SUFFIX}")); let mut path = std::path::PathBuf::from(&command); - + // Check if we can find the sendmail command to execute when no absolute path is given if !path.is_absolute() { - match which::which(&command) { - Ok(result) => path = result, - Err(_) => err!(format!("sendmail command {command:?} not found in $PATH")), - } + let Ok(which_path) = which::which(&command) else { + err!(format!("sendmail command {command} not found in $PATH")) + }; + path = which_path; } match path.metadata() { @@ -932,8 +942,8 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } - if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !cfg.smtp_from.contains('@') { - err!("SMTP_FROM does not contain a mandatory @ sign") + if !is_valid_email(&cfg.smtp_from) { + err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from)) } if cfg._enable_email_2fa && cfg.email_token_size < 6 { @@ -1146,12 +1156,17 @@ impl Config { }) } - pub fn update_config(&self, other: ConfigBuilder) -> Result<(), Error> { + pub fn update_config(&self, other: ConfigBuilder, ignore_non_editable: bool) -> Result<(), Error> { // Remove default values //let builder = other.remove(&self.inner.read().unwrap()._env); // TODO: Remove values that are defaults, above only checks those set by env and not the defaults - let builder = other; + let mut builder = other; + + // Remove values that are not editable + if ignore_non_editable { + builder.clear_non_editable(); + } // Serialize now before we consume the builder let config_str = serde_json::to_string_pretty(&builder)?; @@ -1186,7 +1201,7 @@ impl Config { let mut _overrides = Vec::new(); usr.merge(&other, false, &mut _overrides) }; - self.update_config(builder) + self.update_config(builder, false) } /// Tests whether an email's domain is allowed. A domain is allowed if it diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 6e6ee77a..1aa5fb40 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -152,6 +152,7 @@ impl PartialOrd for i32 { /// Local methods impl Organization { pub fn new(name: String, billing_email: String, private_key: Option, public_key: Option) -> Self { + let billing_email = billing_email.to_lowercase(); Self { uuid: OrganizationId(crate::util::get_uuid()), name, @@ -307,8 +308,8 @@ use crate::error::MapResult; /// Database methods impl Organization { pub async fn save(&self, conn: &mut DbConn) -> EmptyResult { - if !email_address::EmailAddress::is_valid(self.billing_email.trim()) { - err!(format!("BillingEmail {} is not a valid email address", self.billing_email.trim())) + if !crate::util::is_valid_email(&self.billing_email) { + err!(format!("BillingEmail {} is not a valid email address", self.billing_email)) } for member in Membership::find_by_org(&self.uuid, conn).await.iter() { diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 24c86a43..8978fc5a 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -267,8 +267,8 @@ impl User { } pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult { - if self.email.trim().is_empty() { - err!("User email can't be empty") + if !crate::util::is_valid_email(&self.email) { + err!(format!("User email {} is not a valid email address", self.email)) } self.updated_at = Utc::now().naive_utc(); @@ -408,8 +408,8 @@ impl Invitation { } pub async fn save(&self, conn: &mut DbConn) -> EmptyResult { - if self.email.trim().is_empty() { - err!("Invitation email can't be empty") + if !crate::util::is_valid_email(&self.email) { + err!(format!("Invitation email {} is not a valid email address", self.email)) } db_run! {conn: diff --git a/src/mail.rs b/src/mail.rs index 410bac4b..d074995a 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -1,7 +1,6 @@ -use std::str::FromStr; - use chrono::NaiveDateTime; use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; +use std::{env::consts::EXE_SUFFIX, str::FromStr}; use lettre::{ message::{Attachment, Body, Mailbox, Message, MultiPart, SinglePart}, @@ -26,7 +25,7 @@ fn sendmail_transport() -> AsyncSendmailTransport { if let Some(command) = CONFIG.sendmail_command() { AsyncSendmailTransport::new_with_command(command) } else { - AsyncSendmailTransport::new() + AsyncSendmailTransport::new_with_command(format!("sendmail{EXE_SUFFIX}")) } } @@ -595,13 +594,13 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult { // Match some common errors and make them more user friendly Err(e) => { if e.is_client() { - debug!("Sendmail client error: {:#?}", e); + debug!("Sendmail client error: {:?}", e); err!(format!("Sendmail client error: {e}")); } else if e.is_response() { - debug!("Sendmail response error: {:#?}", e); + debug!("Sendmail response error: {:?}", e); err!(format!("Sendmail response error: {e}")); } else { - debug!("Sendmail error: {:#?}", e); + debug!("Sendmail error: {:?}", e); err!(format!("Sendmail error: {e}")); } } diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 258df5e1..566e6a56 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -236,8 +236,11 @@ function checkSecurityHeaders(headers, omit) { "referrer-policy": ["same-origin"], "x-xss-protection": ["0"], "x-robots-tag": ["noindex", "nofollow"], + "cross-origin-resource-policy": ["same-origin"], "content-security-policy": [ - "default-src 'self'", + "default-src 'none'", + "font-src 'self'", + "manifest-src 'self'", "base-uri 'self'", "form-action 'self'", "object-src 'self' blob:", diff --git a/src/util.rs b/src/util.rs index 4a8af5e9..76de40d1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -55,6 +55,8 @@ impl Fairing for AppHeaders { res.set_raw_header("Referrer-Policy", "same-origin"); res.set_raw_header("X-Content-Type-Options", "nosniff"); res.set_raw_header("X-Robots-Tag", "noindex, nofollow"); + res.set_raw_header("Cross-Origin-Resource-Policy", "same-origin"); + // Obsolete in modern browsers, unsafe (XS-Leak), and largely replaced by CSP res.set_raw_header("X-XSS-Protection", "0"); @@ -74,7 +76,9 @@ impl Fairing for AppHeaders { // # Mail Relay: https://bitwarden.com/blog/add-privacy-and-security-using-email-aliases-with-bitwarden/ // app.simplelogin.io, app.addy.io, api.fastmail.com, quack.duckduckgo.com let csp = format!( - "default-src 'self'; \ + "default-src 'none'; \ + font-src 'self'; \ + manifest-src 'self'; \ base-uri 'self'; \ form-action 'self'; \ object-src 'self' blob:; \ @@ -461,6 +465,23 @@ pub fn parse_date(date: &str) -> NaiveDateTime { DateTime::parse_from_rfc3339(date).unwrap().naive_utc() } +/// Returns true or false if an email address is valid or not +/// +/// Some extra checks instead of only using email_address +/// This prevents from weird email formats still excepted but in the end invalid +pub fn is_valid_email(email: &str) -> bool { + let Ok(email) = email_address::EmailAddress::from_str(email) else { + return false; + }; + let Ok(email_url) = url::Url::parse(&format!("https://{}", email.domain())) else { + return false; + }; + if email_url.path().ne("/") || email_url.domain().is_none() || email_url.query().is_some() { + return false; + } + true +} + // // Deployment environment methods //