diff --git a/.env.template b/.env.template index 43748f48..ceb70153 100644 --- a/.env.template +++ b/.env.template @@ -411,6 +411,14 @@ ## Multiple values must be separated with a whitespace. # ALLOWED_IFRAME_ANCESTORS= +## Allowed connect-src (Know the risks!) +## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src +## Allows other domains to URLs which can be loaded using script interfaces like the Forwarded email alias feature +## This adds the configured value to the 'Content-Security-Policy' headers 'connect-src' value. +## Multiple values must be separated with a whitespace. And only HTTPS values are allowed. +## Example: "https://my-addy-io.domain.tld https://my-simplelogin.domain.tld" +# ALLOWED_CONNECT_SRC="" + ## Number of seconds, on average, between login requests from the same IP address before rate limiting kicks in. # LOGIN_RATELIMIT_SECONDS=60 ## Allow a burst of requests of up to this size, while maintaining the average indicated by `LOGIN_RATELIMIT_SECONDS`. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a025041f..7da6b91b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: steps: # Checkout the repo - name: "Checkout" - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 # End Checkout the repo @@ -75,7 +75,7 @@ jobs: # Only install the clippy and rustfmt components on the default rust-toolchain - name: "Install rust-toolchain version" - uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM GMT+2 + uses: dtolnay/rust-toolchain@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa # master @ Nov 18, 2024, 5:36 AM GMT+1 if: ${{ matrix.channel == 'rust-toolchain' }} with: toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" @@ -85,7 +85,7 @@ jobs: # Install the any other channel to be used for which we do not execute clippy and rustfmt - name: "Install MSRV version" - uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM GMT+2 + uses: dtolnay/rust-toolchain@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa # master @ Nov 18, 2024, 5:36 AM GMT+1 if: ${{ matrix.channel != 'rust-toolchain' }} with: toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" @@ -107,7 +107,7 @@ jobs: # End Show environment # Enable Rust Caching - - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 + - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5 with: # Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes. # Like changing the build host from Ubuntu 20.04 to 22.04 for example. @@ -117,6 +117,12 @@ jobs: # Run cargo tests # 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() }} + 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() }} @@ -176,6 +182,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "|Job|Status|" >> $GITHUB_STEP_SUMMARY echo "|---|------|" >> $GITHUB_STEP_SUMMARY + echo "|test (sqlite,mysql,postgresql,enable_mimalloc,query_logger)|${{ steps.test_sqlite_mysql_postgresql_mimalloc_logger.outcome }}|" >> $GITHUB_STEP_SUMMARY echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}|" >> $GITHUB_STEP_SUMMARY echo "|test (sqlite,mysql,postgresql)|${{ steps.test_sqlite_mysql_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY echo "|test (sqlite)|${{ steps.test_sqlite.outcome }}|" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index a671f936..35bb3432 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -13,7 +13,7 @@ jobs: steps: # Checkout the repo - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 # End Checkout the repo # Start Docker Buildx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 22fc4e28..93b8919d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,7 @@ jobs: steps: # Checkout the repo - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 with: fetch-depth: 0 diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 48a8bc1e..4481ec6a 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -28,10 +28,13 @@ jobs: actions: read steps: - name: Checkout code - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@5681af892cd0f4997658e2bacc62bd0a894cf564 # v0.27.0 + uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # v0.29.0 + env: + TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2 + TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1 with: scan-type: repo ignore-unfixed: true @@ -40,6 +43,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@2bbafcdd7fbf96243689e764c2f15d9735164f33 # v3.26.6 + uses: github/codeql-action/upload-sarif@86b04fb0e47484f7282357688f21d5d0e32175fe # v3.27.5 with: sarif_file: 'trivy-results.sarif' diff --git a/Cargo.lock b/Cargo.lock index 9edd20bf..311fa43b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.19" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611cc2ae7d2e242c457e4be7f97036b8ad9ca152b499f53faf99b1ed8fc2553f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" dependencies = [ "brotli", "flate2", @@ -352,9 +352,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bigdecimal" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" dependencies = [ "autocfg", "libm", @@ -369,12 +369,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.6.0" @@ -441,9 +435,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] name = "byteorder" @@ -453,9 +447,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cached" @@ -470,7 +464,7 @@ dependencies = [ "futures", "hashbrown 0.14.5", "once_cell", - "thiserror", + "thiserror 1.0.69", "tokio", "web-time", ] @@ -495,9 +489,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "cc" -version = "1.1.37" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "shlex", ] @@ -510,9 +504,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -586,7 +580,7 @@ checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" dependencies = [ "cookie", "document-features", - "idna 1.0.3", + "idna", "log", "publicsuffix", "serde", @@ -614,9 +608,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -753,7 +747,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ - "bitflags 2.6.0", + "bitflags", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -762,12 +756,12 @@ dependencies = [ [[package]] name = "diesel" -version = "2.2.4" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e" +checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12" dependencies = [ "bigdecimal", - "bitflags 2.6.0", + "bitflags", "byteorder", "chrono", "diesel_derives", @@ -799,9 +793,9 @@ dependencies = [ [[package]] name = "diesel_logger" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23010b507517129dc9b11fb35f36d76fd2d3dd4c85232733697622e345375f2f" +checksum = "8074833fffb675cf22a6ee669124f65f02971e48dd520bb80c7473ff70aeaf95" dependencies = [ "diesel", "log", @@ -886,9 +880,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "email-encoding" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" +checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc" dependencies = [ "base64 0.22.1", "memchr", @@ -932,21 +926,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "version_check", + "windows-sys 0.59.0", ] [[package]] @@ -968,9 +953,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener 5.3.1", "pin-project-lite", @@ -978,15 +963,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fern" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ff9c9d5fb3e6da8ac2f77ab76fe7e8087d512ce095200f8f29ac5b656cf6dc" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" dependencies = [ "libc", "log", @@ -1010,9 +995,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1218,9 +1203,9 @@ dependencies = [ [[package]] name = "governor" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0746aa765db78b521451ef74221663b57ba595bf83f75d0ce23cc09447c8139f" +checksum = "842dc78579ce01e6a1576ad896edc92fca002dd60c9c3746b7fc2bec6fb429d0" dependencies = [ "cfg-if", "dashmap", @@ -1252,35 +1237,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.1.0", + "http 1.2.0", "indexmap", "slab", "tokio", @@ -1306,7 +1272,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "walkdir", ] @@ -1322,9 +1288,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -1346,9 +1312,9 @@ checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hickory-proto" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" +checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5" dependencies = [ "async-trait", "cfg-if", @@ -1357,11 +1323,11 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna 0.4.0", + "idna", "ipnet", "once_cell", "rand", - "thiserror", + "thiserror 1.0.69", "tinyvec", "tokio", "tracing", @@ -1370,9 +1336,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28757f23aa75c98f254cf0405e6d8c25b831b32921b050a66692427679b1f243" +checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4" dependencies = [ "cfg-if", "futures-util", @@ -1384,7 +1350,7 @@ dependencies = [ "rand", "resolv-conf", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -1431,9 +1397,9 @@ dependencies = [ [[package]] name = "html5gum" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91b361633dcc40096d01de35ed535b6089be91880be47b6fd8f560497af7f716" +checksum = "b3918b5f36d61861b757261da986b51be562c7a87ac4e531d4158e67e08bff72" dependencies = [ "jetscii", ] @@ -1451,9 +1417,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -1478,7 +1444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -1489,7 +1455,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "pin-project-lite", ] @@ -1516,7 +1482,6 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1532,15 +1497,15 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.6", - "http 1.1.0", + "h2", + "http 1.2.0", "http-body 1.0.1", "httparse", "itoa", @@ -1557,29 +1522,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http 1.1.0", - "hyper 1.5.0", + "http 1.2.0", + "hyper 1.5.1", "hyper-util", - "rustls 0.23.16", + "rustls 0.23.20", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.31", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "hyper-tls" version = "0.6.0" @@ -1588,7 +1540,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", "native-tls", "tokio", @@ -1605,9 +1557,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.0", + "hyper 1.5.1", "pin-project-lite", "socket2", "tokio", @@ -1762,26 +1714,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -1805,12 +1737,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.2", "serde", ] @@ -1851,9 +1783,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jetscii" @@ -1874,10 +1806,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1922,9 +1855,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lettre" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9" +checksum = "ab4c9a167ff73df98a5ecc07e8bf5ce90b583665da3d1762eb1f775ad4d0d6f5" dependencies = [ "async-std", "async-trait", @@ -1937,7 +1870,7 @@ dependencies = [ "futures-util", "hostname 0.4.0", "httpdate", - "idna 1.0.3", + "idna", "mime", "native-tls", "nom", @@ -1953,9 +1886,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.162" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libm" @@ -1998,9 +1931,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "litrs" @@ -2125,11 +2058,10 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", @@ -2144,7 +2076,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.1.0", + "http 1.2.0", "httparse", "memchr", "mime", @@ -2156,9 +2088,9 @@ dependencies = [ [[package]] name = "mysqlclient-sys" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478e2040dbc35c73927b77a2be91a496de19deab376a6982ed61e89592434619" +checksum = "6bbb9b017b98c4cde5802998113e182eecc1ebce8d47e9ea1697b9a623d53870" dependencies = [ "pkg-config", "vcpkg", @@ -2313,7 +2245,7 @@ version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.6.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -2341,9 +2273,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.4.0+3.4.0" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] @@ -2463,20 +2395,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.14" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.7", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.14" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" dependencies = [ "pest", "pest_generator", @@ -2484,9 +2416,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.14" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" dependencies = [ "pest", "pest_meta", @@ -2497,9 +2429,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.14" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" dependencies = [ "once_cell", "pest", @@ -2610,9 +2542,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "powerfmt" @@ -2640,9 +2572,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2668,28 +2600,28 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "psm" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" dependencies = [ "cc", ] [[package]] name = "publicsuffix" -version = "2.2.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" dependencies = [ - "idna 0.3.0", + "idna", "psl-types", ] [[package]] name = "quanta" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +checksum = "773ce68d0bb9bc7ef20be3536ffe94e223e1f365bd374108b2659fac0c65cfe6" dependencies = [ "crossbeam-utils", "libc", @@ -2768,16 +2700,16 @@ version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -2808,7 +2740,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.8", + "regex-automata 0.4.9", "regex-syntax 0.8.5", ] @@ -2823,9 +2755,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2855,46 +2787,6 @@ dependencies = [ "signal-hook", ] -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.31", - "hyper-tls 0.5.0", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - [[package]] name = "reqwest" version = "0.12.9" @@ -2907,15 +2799,16 @@ dependencies = [ "cookie", "cookie_store", "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2 0.4.6", - "http 1.1.0", + "h2", + "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-rustls", - "hyper-tls 0.6.0", + "hyper-tls", "hyper-util", "ipnet", "js-sys", @@ -2929,8 +2822,8 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", - "system-configuration 0.6.1", + "sync_wrapper", + "system-configuration", "tokio", "tokio-native-tls", "tokio-socks", @@ -3114,15 +3007,15 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "bitflags 2.6.0", + "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3139,9 +3032,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "rustls-pki-types", @@ -3170,9 +3063,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-webpki" @@ -3218,9 +3111,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -3262,7 +3155,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -3281,15 +3174,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -3306,9 +3199,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -3317,9 +3210,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -3412,7 +3305,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -3439,9 +3332,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3513,9 +3406,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -3524,15 +3417,9 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] @@ -3550,47 +3437,25 @@ dependencies = [ [[package]] name = "syslog" -version = "6.1.1" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc7e95b5b795122fafe6519e27629b5ab4232c73ebb2428f568e82b1a457ad3" +checksum = "019f1500a13379b7d051455df397c75770de6311a7a188a699499502704d9f10" dependencies = [ - "error-chain", - "hostname 0.3.1", + "hostname 0.4.0", "libc", "log", "time", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.6.0", + "bitflags", "core-foundation", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -3622,7 +3487,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +dependencies = [ + "thiserror-impl 2.0.7", ] [[package]] @@ -3636,6 +3510,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -3657,9 +3542,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -3680,9 +3565,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -3715,9 +3600,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", @@ -3764,12 +3649,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.16", - "rustls-pki-types", + "rustls 0.23.20", "tokio", ] @@ -3781,15 +3665,15 @@ checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", - "thiserror", + "thiserror 1.0.69", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3810,9 +3694,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -3875,9 +3759,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -3887,9 +3771,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -3898,9 +3782,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -3919,9 +3803,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -3950,12 +3834,12 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.1.0", + "http 1.2.0", "httparse", "log", "rand", "sha1", - "thiserror", + "thiserror 1.0.69", "url", "utf-8", ] @@ -3991,26 +3875,11 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicode-bidi" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" - [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-xid" @@ -4026,12 +3895,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna", "percent-encoding", "serde", ] @@ -4117,7 +3986,7 @@ dependencies = [ "pico-args", "rand", "regex", - "reqwest 0.12.9", + "reqwest", "ring", "rmpv", "rocket", @@ -4177,9 +4046,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -4188,13 +4057,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -4203,21 +4071,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4225,9 +4094,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -4238,9 +4107,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wasm-streams" @@ -4257,9 +4126,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -4289,7 +4158,7 @@ dependencies = [ "serde_cbor", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "tracing", "url", ] @@ -4597,9 +4466,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -4609,9 +4478,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -4621,16 +4490,15 @@ dependencies = [ [[package]] name = "yubico" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173f75d2c4010429a2d74ae3a114a69930c59e2b1a4c97b1c75d259a4960d5fb" +version = "0.12.0" +source = "git+https://github.com/BlackDex/yubico-rs?rev=00df14811f58155c0f02e3ab10f1570ed3e115c6#00df14811f58155c0f02e3ab10f1570ed3e115c6" dependencies = [ - "base64 0.13.1", + "base64 0.22.1", "form_urlencoded", "futures", "hmac", "rand", - "reqwest 0.11.27", + "reqwest", "sha1", "threadpool", ] @@ -4658,18 +4526,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 150b3b9d..f739145b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "vaultwarden" version = "1.0.0" authors = ["Daniel GarcĂ­a "] edition = "2021" -rust-version = "1.80.0" +rust-version = "1.82.0" resolver = "2" repository = "https://github.com/dani-garcia/vaultwarden" @@ -36,13 +36,13 @@ unstable = [] [target."cfg(unix)".dependencies] # Logging -syslog = "6.1.1" +syslog = "7.0.0" [dependencies] # Logging log = "0.4.22" -fern = { version = "0.7.0", features = ["syslog-6", "reopen-1"] } -tracing = { version = "0.1.40", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work +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 # A `dotenv` implementation for Rust dotenvy = { version = "0.15.7", default-features = false } @@ -53,7 +53,7 @@ once_cell = "1.20.2" # Numerical libraries num-traits = "0.2.19" num-derive = "0.4.2" -bigdecimal = "0.4.6" +bigdecimal = "0.4.7" # Web framework rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false } @@ -67,16 +67,16 @@ dashmap = "6.1.0" # Async futures futures = "0.3.31" -tokio = { version = "1.41.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio = { version = "1.42.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } # A generic serialization/deserialization framework -serde = { version = "1.0.214", features = ["derive"] } -serde_json = "1.0.132" +serde = { version = "1.0.216", features = ["derive"] } +serde_json = "1.0.133" # A safe, extensible ORM and Query builder -diesel = { version = "2.2.4", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.2.6", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.2.0" -diesel_logger = { version = "0.3.0", optional = true } +diesel_logger = { version = "0.4.0", optional = true } # Bundled/Static SQLite libsqlite3-sys = { version = "0.30.1", features = ["bundled"], optional = true } @@ -89,9 +89,9 @@ ring = "0.17.8" uuid = { version = "1.11.0", features = ["v4"] } # Date and time libraries -chrono = { version = "0.4.38", features = ["clock", "serde"], default-features = false } +chrono = { version = "0.4.39", features = ["clock", "serde"], default-features = false } chrono-tz = "0.10.0" -time = "0.3.36" +time = "0.3.37" # Job scheduler job_scheduler_ng = "2.0.5" @@ -106,16 +106,16 @@ jsonwebtoken = "9.3.0" totp-lite = "2.0.1" # Yubico Library -yubico = { version = "0.11.0", features = ["online-tokio"], default-features = false } +yubico = { version = "0.12.0", features = ["online-tokio"], default-features = false } # WebAuthn libraries webauthn-rs = "0.3.2" # Handling of URL's for WebAuthn and favicons -url = "2.5.3" +url = "2.5.4" # Email libraries -lettre = { version = "0.11.10", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false } +lettre = { version = "0.11.11", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false } percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails email_address = "0.2.9" @@ -124,13 +124,13 @@ handlebars = { version = "6.2.0", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) reqwest = { version = "0.12.9", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] } -hickory-resolver = "0.24.1" +hickory-resolver = "0.24.2" # Favicon extraction libraries -html5gum = "0.6.1" +html5gum = "0.7.0" regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false } data-url = "0.3.1" -bytes = "1.8.0" +bytes = "1.9.0" # Cache function results (Used for version check and favicon fetching) cached = { version = "0.54.0", features = ["async"] } @@ -147,10 +147,10 @@ pico-args = "0.5.0" # Macro ident concatenation paste = "1.0.15" -governor = "0.7.0" +governor = "0.8.0" # Check client versions for specific features. -semver = "1.0.23" +semver = "1.0.24" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow @@ -166,6 +166,10 @@ rpassword = "7.3.1" # Loading a dynamic CSS Stylesheet grass_compiler = { version = "0.13.4", default-features = false } +[patch.crates-io] +# Patch yubico to remove duplicate crates of older versions +yubico = { git = "https://github.com/BlackDex/yubico-rs", rev = "00df14811f58155c0f02e3ab10f1570ed3e115c6" } + # Strip debuginfo from the release builds # The symbols are the provide better panic traces # Also enable fat LTO and use 1 codegen unit for optimizations @@ -216,7 +220,8 @@ noop_method_call = "deny" refining_impl_trait = { level = "deny", priority = -1 } rust_2018_idioms = { level = "deny", priority = -1 } rust_2021_compatibility = { level = "deny", priority = -1 } -# rust_2024_compatibility = { level = "deny", priority = -1 } # Enable once we are at MSRV 1.81.0 +rust_2024_compatibility = { level = "deny", priority = -1 } +edition_2024_expr_fragment_specifier = "allow" # Once changed to Rust 2024 this should be removed and macro's should be validated again single_use_lifetimes = "deny" trivial_casts = "deny" trivial_numeric_casts = "deny" @@ -225,9 +230,6 @@ unused_import_braces = "deny" unused_lifetimes = "deny" unused_qualifications = "deny" variant_size_differences = "deny" -# The lints below are part of the rust_2024_compatibility group -static-mut-refs = "deny" -unsafe-op-in-unsafe-fn = "deny" # https://rust-lang.github.io/rust-clippy/stable/index.html [lints.clippy] diff --git a/SECURITY.md b/SECURITY.md index 0917981c..4d23e51c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -21,7 +21,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in The following bug classes are out-of scope: - Bugs that are already reported on Vaultwarden's issue tracker (https://github.com/dani-garcia/vaultwarden/issues) -- Bugs that are not part of Vaultwarden, like on the the web-vault or mobile and desktop clients. These issues need to be reported in the respective project issue tracker at https://github.com/bitwarden to which we are not associated +- Bugs that are not part of Vaultwarden, like on the web-vault or mobile and desktop clients. These issues need to be reported in the respective project issue tracker at https://github.com/bitwarden to which we are not associated - Issues in an upstream software dependency (ex: Rust, or External Libraries) which are already reported to the upstream maintainer - Attacks requiring physical access to a user's device - Issues related to software or protocols not under Vaultwarden's control diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index c4c541fb..6896c3dd 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -5,9 +5,9 @@ vault_image_digest: "sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf716 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa" -rust_version: 1.82.0 # Rust version to be used +rust_version: 1.83.0 # Rust version to be used debian_version: bookworm # Debian release name to be used -alpine_version: "3.20" # Alpine version to be used +alpine_version: "3.21" # Alpine version to be used # For which platforms/architectures will we try to build images platforms: ["linux/amd64", "linux/arm64", "linux/arm/v7", "linux/arm/v6"] # Determine the build images per OS/Arch diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index c6c85003..77915fd8 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931 ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.82.0 AS build_amd64 -FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.82.0 AS build_arm64 -FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.82.0 AS build_armv7 -FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.82.0 AS build_armv6 +FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.83.0 AS build_amd64 +FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.83.0 AS build_arm64 +FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.83.0 AS build_armv7 +FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.83.0 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 @@ -126,7 +126,7 @@ RUN source /env-cargo && \ # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 -FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.20 +FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.21 ENV ROCKET_PROFILE="release" \ ROCKET_ADDRESS=0.0.0.0 \ diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index eb502eb2..69404a2e 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:1978e7a58a1777cb0ef0d ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.82.0-slim-bookworm AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.83.0-slim-bookworm AS build COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT diff --git a/docker/README.md b/docker/README.md index 2e78f534..f76cd35d 100644 --- a/docker/README.md +++ b/docker/README.md @@ -46,7 +46,7 @@ There also is an option to use an other docker container to provide support for ```bash # To install and activate docker run --privileged --rm tonistiigi/binfmt --install arm64,arm -# To unistall +# To uninstall docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' ``` diff --git a/docker/docker-bake.hcl b/docker/docker-bake.hcl index 38e7ef97..2edf4fbb 100644 --- a/docker/docker-bake.hcl +++ b/docker/docker-bake.hcl @@ -17,7 +17,7 @@ variable "SOURCE_REPOSITORY_URL" { default = null } -// The commit hash of of the current commit this build was triggered on +// The commit hash of the current commit this build was triggered on variable "SOURCE_COMMIT" { default = null } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a74a99f4..2c76f619 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.82.0" +channel = "1.83.0" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/api/admin.rs b/src/api/admin.rs index cc902e39..f49f6ed3 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -62,6 +62,7 @@ pub fn routes() -> Vec { diagnostics, get_diagnostics_config, resend_user_invite, + get_diagnostics_http, ] } @@ -494,11 +495,11 @@ struct UserOrgTypeData { async fn update_user_org_type(data: Json, token: AdminToken, mut conn: DbConn) -> EmptyResult { let data: UserOrgTypeData = data.into_inner(); - let mut user_to_edit = - match UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await { - Some(user) => user, - None => err!("The specified user isn't member of the organization"), - }; + let Some(mut user_to_edit) = + UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await + else { + err!("The specified user isn't member of the organization") + }; let new_type = match UserOrgType::from_str(&data.user_type.into_string()) { Some(new_type) => new_type as i32, @@ -601,9 +602,8 @@ async fn get_json_api(url: &str) -> Result { } async fn has_http_access() -> bool { - let req = match make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") { - Ok(r) => r, - Err(_) => return false, + let Ok(req) = make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") else { + return false; }; match req.send().await { Ok(r) => r.status().is_success(), @@ -713,6 +713,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) "ip_header_name": ip_header_name, "ip_header_config": &CONFIG.ip_header(), "uses_proxy": uses_proxy, + "enable_websocket": &CONFIG.enable_websocket(), "db_type": *DB_TYPE, "db_version": get_sql_server_version(&mut conn).await, "admin_url": format!("{}/diagnostics", admin_url()), @@ -734,6 +735,11 @@ fn get_diagnostics_config(_token: AdminToken) -> Json { Json(support_json) } +#[get("/diagnostics/http?")] +fn get_diagnostics_http(code: u16, _token: AdminToken) -> EmptyResult { + err_code!(format!("Testing error {code} response"), code); +} + #[post("/config", data = "")] fn post_config(data: Json, _token: AdminToken) -> EmptyResult { let data: ConfigBuilder = data.into_inner(); diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index e9a91efa..2f9e2609 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -607,9 +607,8 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Skip `null` folder id entries. // See: https://github.com/bitwarden/clients/issues/8453 if let Some(folder_id) = folder_data.id { - let saved_folder = match existing_folders.iter_mut().find(|f| f.uuid == folder_id) { - Some(folder) => folder, - None => err!("Folder doesn't exist"), + let Some(saved_folder) = existing_folders.iter_mut().find(|f| f.uuid == folder_id) else { + err!("Folder doesn't exist") }; saved_folder.name = folder_data.name; @@ -619,11 +618,11 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Update emergency access data for emergency_access_data in data.emergency_access_keys { - let saved_emergency_access = - match existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id) { - Some(emergency_access) => emergency_access, - None => err!("Emergency access doesn't exist or is not owned by the user"), - }; + let Some(saved_emergency_access) = + existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id) + else { + err!("Emergency access doesn't exist or is not owned by the user") + }; saved_emergency_access.key_encrypted = Some(emergency_access_data.key_encrypted); saved_emergency_access.save(&mut conn).await? @@ -631,10 +630,10 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Update reset password data for reset_password_data in data.reset_password_keys { - let user_org = match existing_user_orgs.iter_mut().find(|uo| uo.org_uuid == reset_password_data.organization_id) - { - Some(reset_password) => reset_password, - None => err!("Reset password doesn't exist"), + let Some(user_org) = + existing_user_orgs.iter_mut().find(|uo| uo.org_uuid == reset_password_data.organization_id) + else { + err!("Reset password doesn't exist") }; user_org.reset_password_key = Some(reset_password_data.reset_password_key); @@ -643,9 +642,8 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Update send data for send_data in data.sends { - let send = match existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) { - Some(send) => send, - None => err!("Send doesn't exist"), + let Some(send) = existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) else { + err!("Send doesn't exist") }; update_send_from_data(send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?; @@ -656,9 +654,9 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, for cipher_data in data.ciphers { if cipher_data.organization_id.is_none() { - let saved_cipher = match existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap()) { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap()) + else { + err!("Cipher doesn't exist") }; // Prevent triggering cipher updates via WebSockets by settings UpdateType::None @@ -835,14 +833,12 @@ struct VerifyEmailTokenData { async fn post_verify_email_token(data: Json, mut conn: DbConn) -> EmptyResult { let data: VerifyEmailTokenData = data.into_inner(); - let mut user = match User::find_by_uuid(&data.user_id, &mut conn).await { - Some(user) => user, - None => err!("User doesn't exist"), + let Some(mut user) = User::find_by_uuid(&data.user_id, &mut conn).await else { + err!("User doesn't exist") }; - let claims = match decode_verify_email(&data.token) { - Ok(claims) => claims, - Err(_) => err!("Invalid claim"), + let Ok(claims) = decode_verify_email(&data.token) else { + err!("Invalid claim") }; if claims.sub != user.uuid { err!("Invalid claim"); @@ -894,15 +890,14 @@ struct DeleteRecoverTokenData { async fn post_delete_recover_token(data: Json, mut conn: DbConn) -> EmptyResult { let data: DeleteRecoverTokenData = data.into_inner(); - let user = match User::find_by_uuid(&data.user_id, &mut conn).await { - Some(user) => user, - None => err!("User doesn't exist"), + let Ok(claims) = decode_delete(&data.token) else { + err!("Invalid claim") }; - let claims = match decode_delete(&data.token) { - Ok(claims) => claims, - Err(_) => err!("Invalid claim"), + let Some(user) = User::find_by_uuid(&data.user_id, &mut conn).await else { + err!("User doesn't exist") }; + if claims.sub != user.uuid { err!("Invalid claim"); } @@ -1074,11 +1069,8 @@ impl<'r> FromRequest<'r> for KnownDevice { async fn from_request(req: &'r Request<'_>) -> Outcome { let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") { - let email_bytes = match data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) { - Ok(bytes) => bytes, - Err(_) => { - return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as base64url")); - } + let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) else { + return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as base64url")); }; match String::from_utf8(email_bytes) { Ok(email) => email, @@ -1119,9 +1111,9 @@ async fn put_device_token(uuid: &str, data: Json, headers: Headers, m let data = data.into_inner(); let token = data.push_token; - let mut device = match Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await { - Some(device) => device, - None => err!(format!("Error: device {uuid} should be present before a token can be assigned")), + let Some(mut device) = Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await + else { + err!(format!("Error: device {uuid} should be present before a token can be assigned")) }; // if the device already has been registered @@ -1192,9 +1184,8 @@ async fn post_auth_request( ) -> JsonResult { let data = data.into_inner(); - let user = match User::find_by_mail(&data.email, &mut conn).await { - Some(user) => user, - None => err!("AuthRequest doesn't exist", "User not found"), + let Some(user) = User::find_by_mail(&data.email, &mut conn).await else { + err!("AuthRequest doesn't exist", "User not found") }; // Validate device uuid and type @@ -1232,15 +1223,10 @@ async fn post_auth_request( #[get("/auth-requests/")] async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { - Some(auth_request) => auth_request, - None => err!("AuthRequest doesn't exist", "Record not found"), + let Some(auth_request) = AuthRequest::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + err!("AuthRequest doesn't exist", "Record not found or user uuid does not match") }; - if headers.user.uuid != auth_request.user_uuid { - err!("AuthRequest doesn't exist", "User uuid's do not match") - } - let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date)); Ok(Json(json!({ @@ -1277,15 +1263,10 @@ async fn put_auth_request( nt: Notify<'_>, ) -> JsonResult { let data = data.into_inner(); - let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &mut conn).await { - Some(auth_request) => auth_request, - None => err!("AuthRequest doesn't exist", "Record not found"), + let Some(mut auth_request) = AuthRequest::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + err!("AuthRequest doesn't exist", "Record not found or user uuid does not match") }; - if headers.user.uuid != auth_request.user_uuid { - err!("AuthRequest doesn't exist", "User uuid's do not match") - } - if auth_request.approved.is_some() { err!("An authentication request with the same device already exists") } @@ -1330,9 +1311,8 @@ async fn get_auth_request_response( client_headers: ClientHeaders, mut conn: DbConn, ) -> JsonResult { - let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { - Some(auth_request) => auth_request, - None => err!("AuthRequest doesn't exist", "User not found"), + let Some(auth_request) = AuthRequest::find_by_uuid(uuid, &mut conn).await else { + err!("AuthRequest doesn't exist", "User not found") }; if auth_request.device_type != client_headers.device_type diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index aa390e5e..136b890a 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -193,9 +193,8 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { #[get("/ciphers/")] async fn get_cipher(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_accessible_to_user(&headers.user.uuid, &mut conn).await { @@ -429,14 +428,9 @@ pub async fn update_cipher_from_data( cipher.user_uuid = Some(headers.user.uuid.clone()); } - if let Some(ref folder_id) = data.folder_id { - match Folder::find_by_uuid(folder_id, conn).await { - Some(folder) => { - if folder.user_uuid != headers.user.uuid { - err!("Folder is not owned by user") - } - } - None => err!("Folder doesn't exist"), + if let Some(ref folder_uuid) = data.folder_id { + if Folder::find_by_uuid_and_user(folder_uuid, &headers.user.uuid, conn).await.is_none() { + err!("Invalid folder", "Folder does not exist or belongs to another user"); } } @@ -511,7 +505,7 @@ pub async fn update_cipher_from_data( cipher.fields = data.fields.map(|f| _clean_cipher_data(f).to_string()); cipher.data = type_data.to_string(); cipher.password_history = data.password_history.map(|f| f.to_string()); - cipher.reprompt = data.reprompt; + cipher.reprompt = data.reprompt.filter(|r| *r == RepromptType::None as i32 || *r == RepromptType::Password as i32); cipher.save(conn).await?; cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?; @@ -661,9 +655,8 @@ async fn put_cipher( ) -> JsonResult { let data: CipherData = data.into_inner(); - let mut cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(mut cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + err!("Cipher doesn't exist") }; // TODO: Check if only the folder ID or favorite status is being changed. @@ -695,19 +688,13 @@ async fn put_cipher_partial( ) -> JsonResult { let data: PartialCipherData = data.into_inner(); - let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + err!("Cipher doesn't exist") }; - if let Some(ref folder_id) = data.folder_id { - match Folder::find_by_uuid(folder_id, &mut conn).await { - Some(folder) => { - if folder.user_uuid != headers.user.uuid { - err!("Folder is not owned by user") - } - } - None => err!("Folder doesn't exist"), + if let Some(ref folder_uuid) = data.folder_id { + if Folder::find_by_uuid_and_user(folder_uuid, &headers.user.uuid, &mut conn).await.is_none() { + err!("Invalid folder", "Folder does not exist or belongs to another user"); } } @@ -774,9 +761,8 @@ async fn post_collections_update( ) -> JsonResult { let data: CollectionsAdminData = data.into_inner(); - let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await { @@ -788,7 +774,8 @@ async fn post_collections_update( HashSet::::from_iter(cipher.get_collections(headers.user.uuid.clone(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { - match Collection::find_by_uuid(collection, &mut conn).await { + match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await + { None => err!("Invalid collection ID provided"), Some(collection) => { if collection.is_writable_by_user(&headers.user.uuid, &mut conn).await { @@ -851,9 +838,8 @@ async fn post_collections_admin( ) -> EmptyResult { let data: CollectionsAdminData = data.into_inner(); - let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await { @@ -865,7 +851,8 @@ async fn post_collections_admin( HashSet::::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { - match Collection::find_by_uuid(collection, &mut conn).await { + match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await + { None => err!("Invalid collection ID provided"), Some(collection) => { if collection.is_writable_by_user(&headers.user.uuid, &mut conn).await { @@ -1043,9 +1030,8 @@ async fn share_cipher_by_uuid( /// redirects to the same location as before the v2 API. #[get("/ciphers//attachment/")] async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_accessible_to_user(&headers.user.uuid, &mut conn).await { @@ -1084,9 +1070,8 @@ async fn post_attachment_v2( headers: Headers, mut conn: DbConn, ) -> JsonResult { - let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await { @@ -1150,9 +1135,8 @@ async fn save_attachment( err!("Attachment size can't be negative") } - let cipher = match Cipher::find_by_uuid(cipher_uuid, &mut conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await { @@ -1545,21 +1529,15 @@ async fn move_cipher_selected( let data = data.into_inner(); let user_uuid = headers.user.uuid; - if let Some(ref folder_id) = data.folder_id { - match Folder::find_by_uuid(folder_id, &mut conn).await { - Some(folder) => { - if folder.user_uuid != user_uuid { - err!("Folder is not owned by user") - } - } - None => err!("Folder doesn't exist"), + if let Some(ref folder_uuid) = data.folder_id { + if Folder::find_by_uuid_and_user(folder_uuid, &user_uuid, &mut conn).await.is_none() { + err!("Invalid folder", "Folder does not exist or belongs to another user"); } } for uuid in data.ids { - let cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_accessible_to_user(&user_uuid, &mut conn).await { @@ -1667,9 +1645,8 @@ async fn _delete_cipher_by_uuid( soft_delete: bool, nt: &Notify<'_>, ) -> EmptyResult { - let mut cipher = match Cipher::find_by_uuid(uuid, conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await { @@ -1739,9 +1716,8 @@ async fn _delete_multiple_ciphers( } async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>) -> JsonResult { - let mut cipher = match Cipher::find_by_uuid(uuid, conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await { @@ -1807,18 +1783,16 @@ async fn _delete_cipher_attachment_by_id( conn: &mut DbConn, nt: &Notify<'_>, ) -> EmptyResult { - let attachment = match Attachment::find_by_id(attachment_id, conn).await { - Some(attachment) => attachment, - None => err!("Attachment doesn't exist"), + let Some(attachment) = Attachment::find_by_id(attachment_id, conn).await else { + err!("Attachment doesn't exist") }; if attachment.cipher_uuid != uuid { err!("Attachment from other cipher") } - let cipher = match Cipher::find_by_uuid(uuid, conn).await { - Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + let Some(cipher) = Cipher::find_by_uuid(uuid, conn).await else { + err!("Cipher doesn't exist") }; if !cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await { diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 1c29b774..8ed9e87a 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -137,11 +137,11 @@ async fn post_emergency_access( let data: EmergencyAccessUpdateData = data.into_inner(); - let mut emergency_access = - match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await { - Some(emergency_access) => emergency_access, - None => err!("Emergency access not valid."), - }; + let Some(mut emergency_access) = + EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) { Some(new_type) => new_type as i32, @@ -284,24 +284,22 @@ async fn send_invite(data: Json, headers: Headers, mu async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { check_emergency_access_enabled()?; - let mut emergency_access = - match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(mut emergency_access) = + EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; if emergency_access.status != EmergencyAccessStatus::Invited as i32 { err!("The grantee user is already accepted or confirmed to the organization"); } - let email = match emergency_access.email.clone() { - Some(email) => email, - None => err!("Email not valid."), + let Some(email) = emergency_access.email.clone() else { + err!("Email not valid.") }; - let grantee_user = match User::find_by_mail(&email, &mut conn).await { - Some(user) => user, - None => err!("Grantee user not found."), + let Some(grantee_user) = User::find_by_mail(&email, &mut conn).await else { + err!("Grantee user not found.") }; let grantor_user = headers.user; @@ -356,16 +354,15 @@ async fn accept_invite(emer_id: &str, data: Json, headers: Headers, // We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database. // The uuid of the grantee gets stored once accepted. - let mut emergency_access = - match EmergencyAccess::find_by_uuid_and_grantee_email(emer_id, &headers.user.email, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(mut emergency_access) = + EmergencyAccess::find_by_uuid_and_grantee_email(emer_id, &headers.user.email, &mut conn).await + else { + err!("Emergency access not valid.") + }; // get grantor user to send Accepted email - let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), + let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else { + err!("Grantor user not found.") }; if emer_id == claims.emer_id @@ -403,11 +400,11 @@ async fn confirm_emergency_access( let data: ConfirmData = data.into_inner(); let key = data.key; - let mut emergency_access = - match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &confirming_user.uuid, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(mut emergency_access) = + EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &confirming_user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; if emergency_access.status != EmergencyAccessStatus::Accepted as i32 || emergency_access.grantor_uuid != confirming_user.uuid @@ -415,15 +412,13 @@ async fn confirm_emergency_access( err!("Emergency access not valid.") } - let grantor_user = match User::find_by_uuid(&confirming_user.uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), + let Some(grantor_user) = User::find_by_uuid(&confirming_user.uuid, &mut conn).await else { + err!("Grantor user not found.") }; if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { - let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantee user not found."), + let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &mut conn).await else { + err!("Grantee user not found.") }; emergency_access.status = EmergencyAccessStatus::Confirmed as i32; @@ -450,19 +445,18 @@ async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: Db check_emergency_access_enabled()?; let initiating_user = headers.user; - let mut emergency_access = - match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &initiating_user.uuid, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(mut emergency_access) = + EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &initiating_user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 { err!("Emergency access not valid.") } - let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), + let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else { + err!("Grantor user not found.") }; let now = Utc::now().naive_utc(); @@ -488,25 +482,23 @@ async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: Db async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; - let mut emergency_access = - match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(mut emergency_access) = + EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 { err!("Emergency access not valid.") } - let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), + let Some(grantor_user) = User::find_by_uuid(&headers.user.uuid, &mut conn).await else { + err!("Grantor user not found.") }; if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { - let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantee user not found."), + let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &mut conn).await else { + err!("Grantee user not found.") }; emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32; @@ -525,11 +517,11 @@ async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbC async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; - let mut emergency_access = - match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(mut emergency_access) = + EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 && emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32 @@ -538,9 +530,8 @@ async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo } if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { - let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantee user not found."), + let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &mut conn).await else { + err!("Grantee user not found.") }; emergency_access.status = EmergencyAccessStatus::Confirmed as i32; @@ -563,11 +554,11 @@ async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; - let emergency_access = - match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &headers.user.uuid, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(emergency_access) = + EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &headers.user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; if !is_valid_request(&emergency_access, &headers.user.uuid, EmergencyAccessType::View) { err!("Emergency access not valid.") @@ -602,19 +593,18 @@ async fn takeover_emergency_access(emer_id: &str, headers: Headers, mut conn: Db check_emergency_access_enabled()?; let requesting_user = headers.user; - let emergency_access = - match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(emergency_access) = + EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) { err!("Emergency access not valid.") } - let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), + let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else { + err!("Grantor user not found.") }; let result = json!({ @@ -650,19 +640,18 @@ async fn password_emergency_access( //let key = &data.Key; let requesting_user = headers.user; - let emergency_access = - match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(emergency_access) = + EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) { err!("Emergency access not valid.") } - let mut grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), + let Some(mut grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else { + err!("Grantor user not found.") }; // change grantor_user password @@ -686,19 +675,18 @@ async fn password_emergency_access( #[get("/emergency-access//policies")] async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { let requesting_user = headers.user; - let emergency_access = - match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; + let Some(emergency_access) = + EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await + else { + err!("Emergency access not valid.") + }; if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) { err!("Emergency access not valid.") } - let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), + let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else { + err!("Grantor user not found.") }; let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &mut conn); diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index 9766d7a1..3daa2a06 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -25,16 +25,10 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json { #[get("/folders/")] async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let folder = match Folder::find_by_uuid(uuid, &mut conn).await { - Some(folder) => folder, - _ => err!("Invalid folder"), - }; - - if folder.user_uuid != headers.user.uuid { - err!("Folder belongs to another user") + match Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await { + Some(folder) => Ok(Json(folder.to_json())), + _ => err!("Invalid folder", "Folder does not exist or belongs to another user"), } - - Ok(Json(folder.to_json())) } #[derive(Deserialize)] @@ -71,15 +65,10 @@ async fn put_folder( ) -> JsonResult { let data: FolderData = data.into_inner(); - let mut folder = match Folder::find_by_uuid(uuid, &mut conn).await { - Some(folder) => folder, - _ => err!("Invalid folder"), + let Some(mut folder) = Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + err!("Invalid folder", "Folder does not exist or belongs to another user") }; - if folder.user_uuid != headers.user.uuid { - err!("Folder belongs to another user") - } - folder.name = data.name; folder.save(&mut conn).await?; @@ -95,15 +84,10 @@ async fn delete_folder_post(uuid: &str, headers: Headers, conn: DbConn, nt: Noti #[delete("/folders/")] async fn delete_folder(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let folder = match Folder::find_by_uuid(uuid, &mut conn).await { - Some(folder) => folder, - _ => err!("Invalid folder"), + let Some(folder) = Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + err!("Invalid folder", "Folder does not exist or belongs to another user") }; - if folder.user_uuid != headers.user.uuid { - err!("Folder belongs to another user") - } - // Delete the actual folder entry folder.delete(&mut conn).await?; diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 2b51a144..f3158536 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -267,9 +267,8 @@ async fn post_organization( ) -> JsonResult { let data: OrganizationUpdateData = data.into_inner(); - let mut org = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(organization) => organization, - None => err!("Can't find organization details"), + let Some(mut org) = Organization::find_by_uuid(org_id, &mut conn).await else { + err!("Can't find organization details") }; org.name = data.name; @@ -318,9 +317,8 @@ async fn get_org_collections(org_id: &str, _headers: ManagerHeadersLoose, mut co async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { let mut data = Vec::new(); - let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { - Some(u) => u, - None => err!("User is not part of organization"), + let Some(user_org) = UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { + err!("User is not part of organization") }; // get all collection memberships for the current organization @@ -387,9 +385,8 @@ async fn post_organization_collections( ) -> JsonResult { let data: NewCollectionData = data.into_inner(); - let org = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(organization) => organization, - None => err!("Can't find organization details"), + let Some(org) = Organization::find_by_uuid(org_id, &mut conn).await else { + err!("Can't find organization details") }; let collection = Collection::new(org.uuid, data.name, data.external_id); @@ -413,9 +410,8 @@ async fn post_organization_collections( } for user in data.users { - let org_user = match UserOrganization::find_by_uuid(&user.id, &mut conn).await { - Some(u) => u, - None => err!("User is not part of organization"), + let Some(org_user) = UserOrganization::find_by_uuid_and_org(&user.id, org_id, &mut conn).await else { + err!("User is not part of organization") }; if org_user.access_all { @@ -454,20 +450,14 @@ async fn post_organization_collection_update( ) -> JsonResult { let data: NewCollectionData = data.into_inner(); - let org = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(organization) => organization, - None => err!("Can't find organization details"), + if Organization::find_by_uuid(org_id, &mut conn).await.is_none() { + err!("Can't find organization details") }; - let mut collection = match Collection::find_by_uuid(col_id, &mut conn).await { - Some(collection) => collection, - None => err!("Collection not found"), + let Some(mut collection) = Collection::find_by_uuid_and_org(col_id, org_id, &mut conn).await else { + err!("Collection not found") }; - if collection.org_uuid != org.uuid { - err!("Collection is not owned by organization"); - } - collection.name = data.name; collection.external_id = match data.external_id { Some(external_id) if !external_id.trim().is_empty() => Some(external_id), @@ -498,9 +488,8 @@ async fn post_organization_collection_update( CollectionUser::delete_all_by_collection(col_id, &mut conn).await?; for user in data.users { - let org_user = match UserOrganization::find_by_uuid(&user.id, &mut conn).await { - Some(u) => u, - None => err!("User is not part of organization"), + let Some(org_user) = UserOrganization::find_by_uuid_and_org(&user.id, org_id, &mut conn).await else { + err!("User is not part of organization") }; if org_user.access_all { @@ -521,15 +510,8 @@ async fn delete_organization_collection_user( _headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - let collection = match Collection::find_by_uuid(col_id, &mut conn).await { - None => err!("Collection not found"), - Some(collection) => { - if collection.org_uuid == org_id { - collection - } else { - err!("Collection and Organization id 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") }; match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await { @@ -560,26 +542,20 @@ async fn _delete_organization_collection( headers: &ManagerHeaders, conn: &mut DbConn, ) -> EmptyResult { - match Collection::find_by_uuid(col_id, conn).await { - None => err!("Collection not found"), - Some(collection) => { - if collection.org_uuid == org_id { - log_event( - EventType::CollectionDeleted as i32, - &collection.uuid, - org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - conn, - ) - .await; - collection.delete(conn).await - } else { - err!("Collection and Organization id do not match") - } - } - } + let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, conn).await else { + err!("Collection not found", "Collection does not exist or does not belong to this organization") + }; + log_event( + EventType::CollectionDeleted as i32, + &collection.uuid, + org_id, + &headers.user.uuid, + headers.device.atype, + &headers.ip.ip, + conn, + ) + .await; + collection.delete(conn).await } #[delete("/organizations//collections/")] @@ -601,12 +577,11 @@ struct DeleteCollectionData { org_id: String, } -#[post("/organizations//collections//delete", data = "<_data>")] +#[post("/organizations//collections//delete")] async fn post_organization_collection_delete( org_id: &str, col_id: &str, headers: ManagerHeaders, - _data: Json, mut conn: DbConn, ) -> EmptyResult { _delete_organization_collection(org_id, col_id, &headers, &mut conn).await @@ -651,9 +626,9 @@ async fn get_org_collection_detail( err!("Collection is not owned by organization") } - let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { - Some(u) => u, - None => err!("User is not part of organization"), + let Some(user_org) = UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await + else { + err!("User is not part of organization") }; let groups: Vec = if CONFIG.org_groups_enabled() { @@ -695,9 +670,8 @@ async fn get_org_collection_detail( #[get("/organizations//collections//users")] async fn get_collection_users(org_id: &str, coll_id: &str, _headers: ManagerHeaders, mut conn: DbConn) -> JsonResult { // Get org and collection, check that collection is from org - let collection = match Collection::find_by_uuid_and_org(coll_id, org_id, &mut conn).await { - None => err!("Collection not found in Organization"), - Some(collection) => collection, + let Some(collection) = Collection::find_by_uuid_and_org(coll_id, org_id, &mut conn).await else { + err!("Collection not found in Organization") }; let mut user_list = Vec::new(); @@ -731,9 +705,8 @@ async fn put_collection_users( // And then add all the received ones (except if the user has access_all) for d in data.iter() { - let user = match UserOrganization::find_by_uuid(&d.id, &mut conn).await { - Some(u) => u, - None => err!("User is not part of organization"), + let Some(user) = UserOrganization::find_by_uuid_and_org(&d.id, org_id, &mut conn).await else { + err!("User is not part of organization") }; if user.access_all { @@ -1007,18 +980,16 @@ async fn reinvite_user(org_id: &str, user_org: &str, headers: AdminHeaders, mut } async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, conn: &mut DbConn) -> EmptyResult { - let user_org = match UserOrganization::find_by_uuid(user_org, conn).await { - Some(user_org) => user_org, - None => err!("The user hasn't been invited to the organization."), + let Some(user_org) = UserOrganization::find_by_uuid_and_org(user_org, org_id, conn).await else { + err!("The user hasn't been invited to the organization.") }; if user_org.status != UserOrgStatus::Invited as i32 { err!("The user is already accepted or confirmed to the organization") } - let user = match User::find_by_uuid(&user_org.user_uuid, conn).await { - Some(user) => user, - None => err!("User not found."), + let Some(user) = User::find_by_uuid(&user_org.user_uuid, conn).await else { + err!("User not found.") }; if !CONFIG.invitations_allowed() && user.password_hash.is_empty() { @@ -1059,20 +1030,25 @@ struct AcceptData { reset_password_key: Option, } -#[post("/organizations//users/<_org_user_id>/accept", data = "")] -async fn accept_invite(org_id: &str, _org_user_id: &str, data: Json, mut conn: DbConn) -> EmptyResult { +#[post("/organizations//users//accept", data = "")] +async fn accept_invite(org_id: &str, org_user_id: &str, data: Json, mut conn: DbConn) -> EmptyResult { // The web-vault passes org_id and org_user_id in the URL, but we are just reading them from the JWT instead let data: AcceptData = data.into_inner(); let claims = decode_invite(&data.token)?; + // If a claim does not have a user_org_id or it does not match the one in from the URI, something is wrong. + match &claims.user_org_id { + Some(ou_id) if ou_id.eq(org_user_id) => {} + _ => err!("Error accepting the invitation", "Claim does not match the org_user_id"), + } + match User::find_by_mail(&claims.email, &mut conn).await { Some(user) => { Invitation::take(&claims.email, &mut conn).await; if let (Some(user_org), Some(org)) = (&claims.user_org_id, &claims.org_id) { - let mut user_org = match UserOrganization::find_by_uuid_and_org(user_org, org, &mut conn).await { - Some(user_org) => user_org, - None => err!("Error accepting the invitation"), + let Some(mut user_org) = UserOrganization::find_by_uuid_and_org(user_org, org, &mut conn).await else { + err!("Error accepting the invitation") }; if user_org.status != UserOrgStatus::Invited as i32 { @@ -1213,9 +1189,8 @@ async fn _confirm_invite( err!("Key or UserId is not set, unable to process request"); } - let mut user_to_confirm = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { - Some(user) => user, - None => err!("The specified user isn't a member of the organization"), + let Some(mut user_to_confirm) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await else { + err!("The specified user isn't a member of the organization") }; if user_to_confirm.atype != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { @@ -1287,9 +1262,8 @@ async fn get_user( _headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { - let user = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await { - Some(user) => user, - None => err!("The specified user isn't a member of the organization"), + let Some(user) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await else { + err!("The specified user isn't a member of the organization") }; // In this case, when groups are requested we also need to include collections. @@ -1331,14 +1305,12 @@ async fn edit_user( ) -> EmptyResult { let data: EditUserData = data.into_inner(); - let new_type = match UserOrgType::from_str(&data.r#type.into_string()) { - Some(new_type) => new_type, - None => err!("Invalid type"), + let Some(new_type) = UserOrgType::from_str(&data.r#type.into_string()) else { + err!("Invalid type") }; - let mut user_to_edit = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await { - Some(user) => user, - None => err!("The specified user isn't member of the organization"), + let Some(mut user_to_edit) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await else { + err!("The specified user isn't member of the organization") }; if new_type != user_to_edit.atype @@ -1490,9 +1462,8 @@ async fn _delete_user( conn: &mut DbConn, nt: &Notify<'_>, ) -> EmptyResult { - let user_to_delete = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { - Some(user) => user, - None => err!("User to delete isn't member of the organization"), + let Some(user_to_delete) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await else { + err!("User to delete isn't member of the organization") }; if user_to_delete.atype != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { @@ -1652,7 +1623,7 @@ struct BulkCollectionsData { remove_collections: bool, } -// This endpoint is only reachable via the organization view, therefor this endpoint is located here +// This endpoint is only reachable via the organization view, therefore this endpoint is located here // Also Bitwarden does not send out Notifications for these changes, it only does this for individual cipher collection updates #[post("/ciphers/bulk-collections", data = "")] async fn post_bulk_collections(data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { @@ -1725,9 +1696,8 @@ async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> Jso let invite = decode_invite(token)?; - let invite_org_id = match invite.org_id { - Some(invite_org_id) => invite_org_id, - None => err!("Invalid token"), + let Some(invite_org_id) = invite.org_id else { + err!("Invalid token") }; if invite_org_id != org_id { @@ -1747,9 +1717,8 @@ async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> Jso #[get("/organizations//policies/")] async fn get_policy(org_id: &str, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { - let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { - Some(pt) => pt, - None => err!("Invalid or unsupported policy type"), + let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else { + err!("Invalid or unsupported policy type") }; let policy = match OrgPolicy::find_by_org_and_type(org_id, pol_type_enum, &mut conn).await { @@ -1778,9 +1747,8 @@ async fn put_policy( ) -> JsonResult { let data: PolicyData = data.into_inner(); - let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { - Some(pt) => pt, - None => err!("Invalid or unsupported policy type"), + let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else { + err!("Invalid or unsupported policy type") }; // Bitwarden only allows the Reset Password policy when Single Org policy is enabled @@ -2437,9 +2405,8 @@ async fn put_group( err!("Group support is disabled"); } - let group = match Group::find_by_uuid(group_id, &mut conn).await { - Some(group) => group, - None => err!("Group not found"), + let Some(group) = Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await else { + err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; let group_request = data.into_inner(); @@ -2502,15 +2469,14 @@ async fn add_update_group( }))) } -#[get("/organizations/<_org_id>/groups//details")] -async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +#[get("/organizations//groups//details")] +async fn get_group_details(org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } - let group = match Group::find_by_uuid(group_id, &mut conn).await { - Some(group) => group, - _ => err!("Group could not be found!"), + let Some(group) = Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await else { + err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; Ok(Json(group.to_json_details(&mut conn).await)) @@ -2531,9 +2497,8 @@ async fn _delete_group(org_id: &str, group_id: &str, headers: &AdminHeaders, con err!("Group support is disabled"); } - let group = match Group::find_by_uuid(group_id, conn).await { - Some(group) => group, - _ => err!("Group not found"), + let Some(group) = Group::find_by_uuid_and_org(group_id, org_id, conn).await else { + err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; log_event( @@ -2569,29 +2534,27 @@ async fn bulk_delete_groups( Ok(()) } -#[get("/organizations/<_org_id>/groups/")] -async fn get_group(_org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +#[get("/organizations//groups/")] +async fn get_group(org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } - let group = match Group::find_by_uuid(group_id, &mut conn).await { - Some(group) => group, - _ => err!("Group not found"), + let Some(group) = Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await else { + err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; Ok(Json(group.to_json())) } -#[get("/organizations/<_org_id>/groups//users")] -async fn get_group_users(_org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +#[get("/organizations//groups//users")] +async fn get_group_users(org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } - match Group::find_by_uuid(group_id, &mut conn).await { - Some(_) => { /* Do nothing */ } - _ => err!("Group could not be found!"), + if Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await.is_none() { + err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; let group_users: Vec = GroupUser::find_by_group(group_id, &mut conn) @@ -2615,9 +2578,8 @@ async fn put_group_users( err!("Group support is disabled"); } - match Group::find_by_uuid(group_id, &mut conn).await { - Some(_) => { /* Do nothing */ } - _ => err!("Group could not be found!"), + if Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await.is_none() { + err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; GroupUser::delete_all_by_group(group_id, &mut conn).await?; @@ -2642,15 +2604,14 @@ async fn put_group_users( Ok(()) } -#[get("/organizations/<_org_id>/users//groups")] -async fn get_user_groups(_org_id: &str, user_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +#[get("/organizations//users//groups")] +async fn get_user_groups(org_id: &str, user_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } - match UserOrganization::find_by_uuid(user_id, &mut conn).await { - Some(_) => { /* Do nothing */ } - _ => err!("User could not be found!"), + if UserOrganization::find_by_uuid_and_org(user_id, org_id, &mut conn).await.is_none() { + err!("User could not be found!") }; let user_groups: Vec = @@ -2688,13 +2649,8 @@ async fn put_user_groups( err!("Group support is disabled"); } - let user_org = match UserOrganization::find_by_uuid(org_user_id, &mut conn).await { - Some(uo) => uo, - _ => err!("User could not be found!"), - }; - - if user_org.org_uuid != org_id { - err!("Group doesn't belong to organization"); + if UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await.is_none() { + err!("User could not be found or does not belong to the organization."); } GroupUser::delete_all_by_user(org_user_id, &mut conn).await?; @@ -2742,22 +2698,12 @@ async fn delete_group_user( err!("Group support is disabled"); } - let user_org = match UserOrganization::find_by_uuid(org_user_id, &mut conn).await { - Some(uo) => uo, - _ => err!("User could not be found!"), - }; - - if user_org.org_uuid != org_id { - err!("User doesn't belong to organization"); + if UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await.is_none() { + err!("User could not be found or does not belong to the organization."); } - let group = match Group::find_by_uuid(group_id, &mut conn).await { - Some(g) => g, - _ => err!("Group could not be found!"), - }; - - if group.organizations_uuid != org_id { - err!("Group doesn't belong to organization"); + if Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await.is_none() { + err!("Group could not be found or does not belong to the organization."); } log_event( @@ -2789,14 +2735,13 @@ struct OrganizationUserResetPasswordRequest { key: String, } -// Upstrem reports this is the renamed endpoint instead of `/keys` +// Upstream reports this is the renamed endpoint instead of `/keys` // 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: &str, _headers: Headers, mut conn: DbConn) -> JsonResult { - let org = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(organization) => organization, - None => err!("Organization not found"), + let Some(org) = Organization::find_by_uuid(org_id, &mut conn).await else { + err!("Organization not found") }; Ok(Json(json!({ @@ -2821,19 +2766,16 @@ async fn put_reset_password( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let org = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(org) => org, - None => err!("Required organization not found"), + let Some(org) = Organization::find_by_uuid(org_id, &mut conn).await else { + err!("Required organization not found") }; - let org_user = match UserOrganization::find_by_uuid_and_org(org_user_id, &org.uuid, &mut conn).await { - Some(user) => user, - None => err!("User to reset isn't member of required organization"), + let Some(org_user) = UserOrganization::find_by_uuid_and_org(org_user_id, &org.uuid, &mut conn).await else { + err!("User to reset isn't member of required organization") }; - let user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { - Some(user) => user, - None => err!("User not found"), + let Some(user) = User::find_by_uuid(&org_user.user_uuid, &mut conn).await else { + err!("User not found") }; check_reset_password_applicable_and_permissions(org_id, org_user_id, &headers, &mut conn).await?; @@ -2880,19 +2822,16 @@ async fn get_reset_password_details( headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { - let org = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(org) => org, - None => err!("Required organization not found"), + let Some(org) = Organization::find_by_uuid(org_id, &mut conn).await else { + err!("Required organization not found") }; - let org_user = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await { - Some(user) => user, - None => err!("User to reset isn't member of required organization"), + let Some(org_user) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await else { + err!("User to reset isn't member of required organization") }; - let user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { - Some(user) => user, - None => err!("User not found"), + let Some(user) = User::find_by_uuid(&org_user.user_uuid, &mut conn).await else { + err!("User not found") }; check_reset_password_applicable_and_permissions(org_id, org_user_id, &headers, &mut conn).await?; @@ -2918,9 +2857,8 @@ async fn check_reset_password_applicable_and_permissions( ) -> EmptyResult { check_reset_password_applicable(org_id, conn).await?; - let target_user = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { - Some(user) => user, - None => err!("Reset target user not found"), + let Some(target_user) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await else { + err!("Reset target user not found") }; // Resetting user must be higher/equal to user to reset @@ -2936,9 +2874,8 @@ async fn check_reset_password_applicable(org_id: &str, conn: &mut DbConn) -> Emp err!("Password reset is not supported on an email-disabled instance."); } - let policy = match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, conn).await { - Some(p) => p, - None => err!("Policy not found"), + let Some(policy) = OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, conn).await else { + err!("Policy not found") }; if !policy.enabled { @@ -2956,9 +2893,8 @@ async fn put_reset_password_enrollment( data: Json, mut conn: DbConn, ) -> EmptyResult { - let mut org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { - Some(u) => u, - None => err!("User to enroll isn't member of required organization"), + let Some(mut org_user) = UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { + err!("User to enroll isn't member of required organization") }; check_reset_password_applicable(org_id, &mut conn).await?; diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 737d30dd..3b3e74cb 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -203,9 +203,8 @@ impl<'r> FromRequest<'r> for PublicToken { None => err_handler!("No access token provided"), }; // Check JWT token is valid and get device and user from it - let claims = match auth::decode_api_org(access_token) { - Ok(claims) => claims, - Err(_) => err_handler!("Invalid claim"), + let Ok(claims) = auth::decode_api_org(access_token) else { + err_handler!("Invalid claim") }; // Check if time is between claims.nbf and claims.exp let time_now = Utc::now().timestamp(); @@ -227,13 +226,11 @@ impl<'r> FromRequest<'r> for PublicToken { Outcome::Success(conn) => conn, _ => err_handler!("Error getting DB"), }; - let org_uuid = match claims.client_id.strip_prefix("organization.") { - Some(uuid) => uuid, - None => err_handler!("Malformed client_id"), + let Some(org_uuid) = claims.client_id.strip_prefix("organization.") else { + err_handler!("Malformed client_id") }; - let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await { - Some(org_api_key) => org_api_key, - None => err_handler!("Invalid client_id"), + let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await else { + err_handler!("Invalid client_id") }; if org_api_key.org_uuid != claims.client_sub { err_handler!("Token not issued for this org"); diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index a7e5bcf0..b98ecf70 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -159,16 +159,10 @@ async fn get_sends(headers: Headers, mut conn: DbConn) -> Json { #[get("/sends/")] async fn get_send(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let send = match Send::find_by_uuid(uuid, &mut conn).await { - Some(send) => send, - None => err!("Send not found"), - }; - - if send.user_uuid.as_ref() != Some(&headers.user.uuid) { - err!("Send is not owned by user") + match Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await { + Some(send) => Ok(Json(send.to_json())), + None => err!("Send not found", "Invalid uuid or does not belong to user"), } - - Ok(Json(send.to_json())) } #[post("/sends", data = "")] @@ -371,22 +365,14 @@ async fn post_send_file_v2_data( let mut data = data.into_inner(); - let Some(send) = Send::find_by_uuid(send_uuid, &mut conn).await else { - err!("Send not found. Unable to save the file.") + let Some(send) = Send::find_by_uuid_and_user(send_uuid, &headers.user.uuid, &mut conn).await else { + err!("Send not found. Unable to save the file.", "Invalid uuid or does not belong to user.") }; if send.atype != SendType::File as i32 { err!("Send is not a file type send."); } - let Some(send_user_id) = &send.user_uuid else { - err!("Sends are only supported for users at the moment.") - }; - - if send_user_id != &headers.user.uuid { - err!("Send doesn't belong to user."); - } - let Ok(send_data) = serde_json::from_str::(&send.data) else { err!("Unable to decode send data as json.") }; @@ -456,9 +442,8 @@ async fn post_access( ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { - let mut send = match Send::find_by_access_id(access_id, &mut conn).await { - Some(s) => s, - None => err_code!(SEND_INACCESSIBLE_MSG, 404), + let Some(mut send) = Send::find_by_access_id(access_id, &mut conn).await else { + err_code!(SEND_INACCESSIBLE_MSG, 404) }; if let Some(max_access_count) = send.max_access_count { @@ -517,9 +502,8 @@ async fn post_access_file( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - let mut send = match Send::find_by_uuid(send_id, &mut conn).await { - Some(s) => s, - None => err_code!(SEND_INACCESSIBLE_MSG, 404), + let Some(mut send) = Send::find_by_uuid(send_id, &mut conn).await else { + err_code!(SEND_INACCESSIBLE_MSG, 404) }; if let Some(max_access_count) = send.max_access_count { @@ -582,16 +566,15 @@ async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Opt None } -#[put("/sends/", data = "")] -async fn put_send(id: &str, data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { +#[put("/sends/", data = "")] +async fn put_send(uuid: &str, data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { enforce_disable_send_policy(&headers, &mut conn).await?; let data: SendData = data.into_inner(); enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?; - let mut send = match Send::find_by_uuid(id, &mut conn).await { - Some(s) => s, - None => err!("Send not found"), + let Some(mut send) = Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + err!("Send not found", "Send uuid is invalid or does not belong to user") }; update_send_from_data(&mut send, data, &headers, &mut conn, &nt, UpdateType::SyncSendUpdate).await?; @@ -657,17 +640,12 @@ pub async fn update_send_from_data( Ok(()) } -#[delete("/sends/")] -async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let send = match Send::find_by_uuid(id, &mut conn).await { - Some(s) => s, - None => err!("Send not found"), +#[delete("/sends/")] +async fn delete_send(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let Some(send) = Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + err!("Send not found", "Invalid send uuid, or does not belong to user") }; - if send.user_uuid.as_ref() != Some(&headers.user.uuid) { - err!("Send is not owned by user") - } - send.delete(&mut conn).await?; nt.send_send_update( UpdateType::SyncSendDelete, @@ -681,19 +659,14 @@ async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_ Ok(()) } -#[put("/sends//remove-password")] -async fn put_remove_password(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { +#[put("/sends//remove-password")] +async fn put_remove_password(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { enforce_disable_send_policy(&headers, &mut conn).await?; - let mut send = match Send::find_by_uuid(id, &mut conn).await { - Some(s) => s, - None => err!("Send not found"), + let Some(mut send) = Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + err!("Send not found", "Invalid send uuid, or does not belong to user") }; - if send.user_uuid.as_ref() != Some(&headers.user.uuid) { - err!("Send is not owned by user") - } - send.set_password(None); send.save(&mut conn).await?; nt.send_send_update( diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 9d4bd480..64cc6486 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -117,9 +117,8 @@ pub async fn validate_totp_code( ) -> EmptyResult { use totp_lite::{totp_custom, Sha1}; - let decoded_secret = match BASE32.decode(secret.as_bytes()) { - Ok(s) => s, - Err(_) => err!("Invalid TOTP secret"), + let Ok(decoded_secret) = BASE32.decode(secret.as_bytes()) else { + err!("Invalid TOTP secret") }; let mut twofactor = diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 6de2935d..76421043 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -232,9 +232,8 @@ async fn get_user_duo_data(uuid: &str, conn: &mut DbConn) -> DuoStatus { let type_ = TwoFactorType::Duo as i32; // If the user doesn't have an entry, disabled - let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, conn).await { - Some(t) => t, - None => return DuoStatus::Disabled(DuoData::global().is_some()), + let Some(twofactor) = TwoFactor::find_by_user_and_type(uuid, type_, conn).await else { + return DuoStatus::Disabled(DuoData::global().is_some()); }; // If the user has the required values, we use those @@ -333,14 +332,12 @@ fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) - err!("Prefixes don't match") } - let cookie_vec = match BASE64.decode(u_b64.as_bytes()) { - Ok(c) => c, - Err(_) => err!("Invalid Duo cookie encoding"), + let Ok(cookie_vec) = BASE64.decode(u_b64.as_bytes()) else { + err!("Invalid Duo cookie encoding") }; - let cookie = match String::from_utf8(cookie_vec) { - Ok(c) => c, - Err(_) => err!("Invalid Duo cookie encoding"), + let Ok(cookie) = String::from_utf8(cookie_vec) else { + err!("Invalid Duo cookie encoding") }; let cookie_split: Vec<&str> = cookie.split('|').collect(); diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 293c0671..09f2f3b7 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -40,9 +40,8 @@ async fn send_email_login(data: Json, mut conn: DbConn) -> E use crate::db::models::User; // Get the user - let user = match User::find_by_mail(&data.email, &mut conn).await { - Some(user) => user, - None => err!("Username or password is incorrect. Try again."), + let Some(user) = User::find_by_mail(&data.email, &mut conn).await else { + err!("Username or password is incorrect. Try again.") }; // Check password @@ -174,9 +173,8 @@ async fn email(data: Json, headers: Headers, mut conn: DbConn) -> Jso let mut email_data = EmailTokenData::from_json(&twofactor.data)?; - let issued_token = match &email_data.last_token { - Some(t) => t, - _ => err!("No token available"), + let Some(issued_token) = &email_data.last_token else { + err!("No token available") }; if !crypto::ct_eq(issued_token, data.token) { @@ -205,14 +203,13 @@ pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, c let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Email as i32, conn) .await .map_res("Two factor not found")?; - let issued_token = match &email_data.last_token { - Some(t) => t, - _ => err!( + let Some(issued_token) = &email_data.last_token else { + err!( "No token available", ErrorEvent { event: EventType::UserFailedLogIn2fa } - ), + ) }; if !crypto::ct_eq(issued_token, token) { diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index e3795eb8..486b526a 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -85,9 +85,8 @@ async fn recover(data: Json, client_headers: ClientHeaders, mu use crate::db::models::User; // Get the user - let mut user = match User::find_by_mail(&data.email, &mut conn).await { - Some(user) => user, - None => err!("Username or password is incorrect. Try again."), + let Some(mut user) = User::find_by_mail(&data.email, &mut conn).await else { + err!("Username or password is incorrect. Try again.") }; // Check password diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 52ca70c4..9ee83d38 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -309,17 +309,16 @@ async fn delete_webauthn(data: Json, headers: Headers, mut conn: err!("Invalid password"); } - let mut tf = - match TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &mut conn).await { - Some(tf) => tf, - None => err!("Webauthn data not found!"), - }; + let Some(mut tf) = + TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &mut conn).await + else { + err!("Webauthn data not found!") + }; let mut data: Vec = serde_json::from_str(&tf.data)?; - let item_pos = match data.iter().position(|r| r.id == id) { - Some(p) => p, - None => err!("Webauthn entry not found"), + let Some(item_pos) = data.iter().position(|r| r.id == id) else { + err!("Webauthn entry not found") }; let removed_item = data.remove(item_pos); diff --git a/src/api/icons.rs b/src/api/icons.rs index 6afbaa9f..921d48b9 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -19,7 +19,7 @@ use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, }; -use html5gum::{Emitter, HtmlString, InfallibleTokenizer, Readable, StringReader, Tokenizer}; +use html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer}; use crate::{ error::Error, @@ -261,11 +261,7 @@ impl Icon { } } -fn get_favicons_node( - dom: InfallibleTokenizer, FaviconEmitter>, - icons: &mut Vec, - url: &url::Url, -) { +fn get_favicons_node(dom: Tokenizer, FaviconEmitter>, icons: &mut Vec, url: &url::Url) { const TAG_LINK: &[u8] = b"link"; const TAG_BASE: &[u8] = b"base"; const TAG_HEAD: &[u8] = b"head"; @@ -274,7 +270,7 @@ fn get_favicons_node( let mut base_url = url.clone(); let mut icon_tags: Vec = Vec::new(); - for token in dom { + for Ok(token) in dom { let tag_name: &[u8] = &token.tag.name; match tag_name { TAG_LINK => { @@ -401,7 +397,7 @@ async fn get_icon_url(domain: &str) -> Result { // 384KB should be more than enough for the HTML, though as we only really need the HTML header. let limited_reader = stream_to_bytes_limit(content, 384 * 1024).await?.to_vec(); - let dom = Tokenizer::new_with_emitter(limited_reader.to_reader(), FaviconEmitter::default()).infallible(); + let dom = Tokenizer::new_with_emitter(limited_reader.to_reader(), FaviconEmitter::default()); get_favicons_node(dom, &mut iconlist, &url); } else { // Add the default favicon.ico to the list with just the given domain @@ -662,7 +658,7 @@ impl reqwest::cookie::CookieStore for Jar { /// The FaviconEmitter is using an optimized version of the DefaultEmitter. /// This prevents emitting tags like comments, doctype and also strings between the tags. /// But it will also only emit the tags we need and only if they have the correct attributes -/// Therefor parsing the HTML content is faster. +/// Therefore parsing the HTML content is faster. use std::collections::BTreeMap; #[derive(Default)] diff --git a/src/api/identity.rs b/src/api/identity.rs index 02c8529c..82841e82 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -157,9 +157,8 @@ async fn _password_login( // Get the user let username = data.username.as_ref().unwrap().trim(); - let mut user = match User::find_by_mail(username, conn).await { - Some(user) => user, - None => err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)), + let Some(mut user) = User::find_by_mail(username, conn).await else { + err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)) }; // Set the user_uuid here to be passed back used for event logging. @@ -180,7 +179,8 @@ async fn _password_login( // If we get an auth request, we don't check the user's password, but the access code of the auth request if let Some(ref auth_request_uuid) = data.auth_request { - let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await else { + let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_uuid.as_str(), &user.uuid, conn).await + else { err!( "Auth request not found. Try again.", format!("IP: {}. Username: {}.", ip.ip, username), @@ -382,13 +382,11 @@ async fn _user_api_key_login( ) -> JsonResult { // Get the user via the client_id let client_id = data.client_id.as_ref().unwrap(); - let client_user_uuid = match client_id.strip_prefix("user.") { - Some(uuid) => uuid, - None => err!("Malformed client_id", format!("IP: {}.", ip.ip)), + let Some(client_user_uuid) = client_id.strip_prefix("user.") else { + err!("Malformed client_id", format!("IP: {}.", ip.ip)) }; - let user = match User::find_by_uuid(client_user_uuid, conn).await { - Some(user) => user, - None => err!("Invalid client_id", format!("IP: {}.", ip.ip)), + let Some(user) = User::find_by_uuid(client_user_uuid, conn).await else { + err!("Invalid client_id", format!("IP: {}.", ip.ip)) }; // Set the user_uuid here to be passed back used for event logging. @@ -471,13 +469,11 @@ async fn _user_api_key_login( async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) -> JsonResult { // Get the org via the client_id let client_id = data.client_id.as_ref().unwrap(); - let org_uuid = match client_id.strip_prefix("organization.") { - Some(uuid) => uuid, - None => err!("Malformed client_id", format!("IP: {}.", ip.ip)), + let Some(org_uuid) = client_id.strip_prefix("organization.") else { + err!("Malformed client_id", format!("IP: {}.", ip.ip)) }; - let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, conn).await { - Some(org_api_key) => org_api_key, - None => err!("Invalid client_id", format!("IP: {}.", ip.ip)), + let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(org_uuid, conn).await else { + err!("Invalid client_id", format!("IP: {}.", ip.ip)) }; // Check API key. @@ -676,9 +672,8 @@ async fn _json_err_twofactor( } Some(tf_type @ TwoFactorType::YubiKey) => { - let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await { - Some(tf) => tf, - None => err!("No YubiKey devices registered"), + let Some(twofactor) = TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await else { + err!("No YubiKey devices registered") }; let yubikey_metadata: yubikey::YubikeyMetadata = serde_json::from_str(&twofactor.data)?; @@ -689,9 +684,8 @@ async fn _json_err_twofactor( } Some(tf_type @ TwoFactorType::Email) => { - let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await { - Some(tf) => tf, - None => err!("No twofactor email registered"), + let Some(twofactor) = TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await else { + err!("No twofactor email registered") }; // Send email immediately if email is the only 2FA option diff --git a/src/api/push.rs b/src/api/push.rs index d6abfc49..7396a68d 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -155,12 +155,9 @@ pub async fn push_cipher_update( if cipher.organization_uuid.is_some() { return; }; - let user_uuid = match &cipher.user_uuid { - Some(c) => c, - None => { - debug!("Cipher has no uuid"); - return; - } + let Some(user_uuid) = &cipher.user_uuid else { + debug!("Cipher has no uuid"); + return; }; if Device::check_user_has_push_device(user_uuid, conn).await { diff --git a/src/auth.rs b/src/auth.rs index 44b2adbe..79fc5594 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -503,9 +503,8 @@ impl<'r> FromRequest<'r> for Headers { }; // Check JWT token is valid and get device and user from it - let claims = match decode_login(access_token) { - Ok(claims) => claims, - Err(_) => err_handler!("Invalid claim"), + let Ok(claims) = decode_login(access_token) else { + err_handler!("Invalid claim") }; let device_uuid = claims.device; @@ -516,23 +515,20 @@ impl<'r> FromRequest<'r> for Headers { _ => err_handler!("Error getting DB"), }; - let device = match Device::find_by_uuid_and_user(&device_uuid, &user_uuid, &mut conn).await { - Some(device) => device, - None => err_handler!("Invalid device id"), + let Some(device) = Device::find_by_uuid_and_user(&device_uuid, &user_uuid, &mut conn).await else { + err_handler!("Invalid device id") }; - let user = match User::find_by_uuid(&user_uuid, &mut conn).await { - Some(user) => user, - None => err_handler!("Device has no user associated"), + let Some(user) = User::find_by_uuid(&user_uuid, &mut conn).await else { + err_handler!("Device has no user associated") }; if user.security_stamp != claims.sstamp { if let Some(stamp_exception) = user.stamp_exception.as_deref().and_then(|s| serde_json::from_str::(s).ok()) { - let current_route = match request.route().and_then(|r| r.name.as_deref()) { - Some(name) => name, - _ => err_handler!("Error getting current route for stamp exception"), + let Some(current_route) = request.route().and_then(|r| r.name.as_deref()) else { + err_handler!("Error getting current route for stamp exception") }; // Check if the stamp exception has expired first. diff --git a/src/config.rs b/src/config.rs index c4a8a005..22021238 100644 --- a/src/config.rs +++ b/src/config.rs @@ -238,6 +238,7 @@ macro_rules! make_config { // Besides Pass, only String types will be masked via _privacy_mask. const PRIVACY_CONFIG: &[&str] = &[ "allowed_iframe_ancestors", + "allowed_connect_src", "database_url", "domain_origin", "domain_path", @@ -248,6 +249,7 @@ macro_rules! make_config { "smtp_from", "smtp_host", "smtp_username", + "_smtp_img_src", ]; let cfg = { @@ -610,6 +612,9 @@ make_config! { /// Allowed iframe ancestors (Know the risks!) |> Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets allowed_iframe_ancestors: String, true, def, String::new(); + /// Allowed connect-src (Know the risks!) |> Allows other domains to URLs which can be loaded using script interfaces like the Forwarded email alias feature + allowed_connect_src: String, true, def, String::new(); + /// Seconds between login requests |> Number of seconds, on average, between login and 2FA requests from the same IP address before rate limiting kicks in login_ratelimit_seconds: u64, false, def, 60; /// Max burst size for login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `login_ratelimit_seconds`. Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2 @@ -761,6 +766,13 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { ); } + let connect_src = cfg.allowed_connect_src.to_lowercase(); + for url in connect_src.split_whitespace() { + if !url.starts_with("https://") || Url::parse(url).is_err() { + err!("ALLOWED_CONNECT_SRC variable contains one or more invalid URLs. Only FQDN's starting with https are allowed"); + } + } + let whitelist = &cfg.signups_domains_whitelist; if !whitelist.is_empty() && whitelist.split(',').any(|d| d.trim().is_empty()) { err!("`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens"); diff --git a/src/db/mod.rs b/src/db/mod.rs index fe1ab79b..464be561 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -373,24 +373,18 @@ pub async fn backup_database(conn: &mut DbConn) -> Result { err!("PostgreSQL and MySQL/MariaDB do not support this backup feature"); } sqlite { - backup_sqlite_database(conn) + let db_url = CONFIG.database_url(); + let db_path = std::path::Path::new(&db_url).parent().unwrap(); + let backup_file = db_path + .join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S"))) + .to_string_lossy() + .into_owned(); + diesel::sql_query(format!("VACUUM INTO '{backup_file}'")).execute(conn)?; + Ok(backup_file) } } } -#[cfg(sqlite)] -pub fn backup_sqlite_database(conn: &mut diesel::sqlite::SqliteConnection) -> Result { - use diesel::RunQueryDsl; - let db_url = CONFIG.database_url(); - let db_path = std::path::Path::new(&db_url).parent().unwrap(); - let backup_file = db_path - .join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S"))) - .to_string_lossy() - .into_owned(); - diesel::sql_query(format!("VACUUM INTO '{backup_file}'")).execute(conn)?; - Ok(backup_file) -} - /// Get the SQL Server version pub async fn get_sql_server_version(conn: &mut DbConn) -> String { db_run! {@raw conn: diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index 9388c71a..3aca20cb 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -111,6 +111,17 @@ impl AuthRequest { }} } + pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option { + db_run! {conn: { + auth_requests::table + .filter(auth_requests::uuid.eq(uuid)) + .filter(auth_requests::user_uuid.eq(user_uuid)) + .first::(conn) + .ok() + .from_db() + }} + } + pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { db_run! {conn: { auth_requests::table diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 75d54df2..048cb4d1 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -46,10 +46,9 @@ db_object! { } } -#[allow(dead_code)] pub enum RepromptType { None = 0, - Password = 1, // not currently used in server + Password = 1, } /// Local methods @@ -296,7 +295,7 @@ impl Cipher { "creationDate": format_date(&self.created_at), "revisionDate": format_date(&self.updated_at), "deletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))), - "reprompt": self.reprompt.unwrap_or(RepromptType::None as i32), + "reprompt": self.reprompt.filter(|r| *r == RepromptType::None as i32 || *r == RepromptType::Password as i32).unwrap_or(RepromptType::None as i32), "organizationId": self.organization_uuid, "key": self.key, "attachments": attachments_json, diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 5370c9dd..45460720 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -120,10 +120,11 @@ impl Folder { Ok(()) } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option { db_run! { conn: { folders::table .filter(folders::uuid.eq(uuid)) + .filter(folders::user_uuid.eq(user_uuid)) .first::(conn) .ok() .from_db() diff --git a/src/db/models/group.rs b/src/db/models/group.rs index e226512d..d9a08970 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -191,10 +191,11 @@ impl Group { }} } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { db_run! { conn: { groups::table .filter(groups::uuid.eq(uuid)) + .filter(groups::organizations_uuid.eq(org_uuid)) .first::(conn) .ok() .from_db() diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index c336cb1a..43595bee 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -18,7 +18,7 @@ mod user; pub use self::attachment::Attachment; pub use self::auth_request::AuthRequest; -pub use self::cipher::Cipher; +pub use self::cipher::{Cipher, RepromptType}; pub use self::collection::{Collection, CollectionCipher, CollectionUser}; pub use self::device::{Device, DeviceType}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 23e583b4..14447c05 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -142,16 +142,6 @@ impl OrgPolicy { }} } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { - db_run! { conn: { - org_policies::table - .filter(org_policies::uuid.eq(uuid)) - .first::(conn) - .ok() - .from_db() - }} - } - pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec { db_run! { conn: { org_policies::table diff --git a/src/db/models/send.rs b/src/db/models/send.rs index 36944281..fb95f86b 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -268,9 +268,8 @@ impl Send { use data_encoding::BASE64URL_NOPAD; use uuid::Uuid; - let uuid_vec = match BASE64URL_NOPAD.decode(access_id.as_bytes()) { - Ok(v) => v, - Err(_) => return None, + let Ok(uuid_vec) = BASE64URL_NOPAD.decode(access_id.as_bytes()) else { + return None; }; let uuid = match Uuid::from_slice(&uuid_vec) { @@ -291,6 +290,17 @@ impl Send { }} } + pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option { + db_run! {conn: { + sends::table + .filter(sends::uuid.eq(uuid)) + .filter(sends::user_uuid.eq(user_uuid)) + .first::(conn) + .ok() + .from_db() + }} + } + pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { db_run! {conn: { sends::table diff --git a/src/mail.rs b/src/mail.rs index 1754400b..3cd6f679 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -308,9 +308,8 @@ pub async fn send_invite( } } - let query_string = match query.query() { - None => err!("Failed to build invite URL query parameters"), - Some(query) => query, + let Some(query_string) = query.query() else { + err!("Failed to build invite URL query parameters") }; let (subject, body_html, body_text) = get_text( @@ -352,9 +351,8 @@ pub async fn send_emergency_access_invite( .append_pair("token", &encode_jwt(&claims)); } - let query_string = match query.query() { - None => err!("Failed to build emergency invite URL query parameters"), - Some(query) => query, + let Some(query_string) = query.query() else { + err!("Failed to build emergency invite URL query parameters") }; let (subject, body_html, body_text) = get_text( diff --git a/src/main.rs b/src/main.rs index 7e180e2e..3a151cdf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,7 +67,7 @@ pub use util::is_running_in_container; #[rocket::main] async fn main() -> Result<(), Error> { - parse_args(); + parse_args().await; launch_info(); let level = init_logging()?; @@ -115,7 +115,7 @@ PRESETS: m= t= p= pub const VERSION: Option<&str> = option_env!("VW_VERSION"); -fn parse_args() { +async fn parse_args() { let mut pargs = pico_args::Arguments::from_env(); let version = VERSION.unwrap_or("(Version info from Git not present)"); @@ -186,7 +186,7 @@ fn parse_args() { exit(1); } } else if command == "backup" { - match backup_sqlite() { + match backup_sqlite().await { Ok(f) => { println!("Backup to '{f}' was successful"); exit(0); @@ -201,25 +201,20 @@ fn parse_args() { } } -fn backup_sqlite() -> Result { - #[cfg(sqlite)] - { - use crate::db::{backup_sqlite_database, DbConnType}; - if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false) { - use diesel::Connection; - let url = CONFIG.database_url(); +async fn backup_sqlite() -> Result { + use crate::db::{backup_database, DbConnType}; + if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false) { + // Establish a connection to the sqlite database + let mut conn = db::DbPool::from_config() + .expect("SQLite database connection failed") + .get() + .await + .expect("Unable to get SQLite db pool"); - // Establish a connection to the sqlite database - let mut conn = diesel::sqlite::SqliteConnection::establish(&url)?; - let backup_file = backup_sqlite_database(&mut conn)?; - Ok(backup_file) - } else { - err_silent!("The database type is not SQLite. Backups only works for SQLite databases") - } - } - #[cfg(not(sqlite))] - { - err_silent!("The 'sqlite' feature is not enabled. Backups only works for SQLite databases") + let backup_file = backup_database(&mut conn).await?; + Ok(backup_file) + } else { + err_silent!("The database type is not SQLite. Backups only works for SQLite databases") } } @@ -610,7 +605,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> // If we need more signals to act upon, we might want to use select! here. // With only one item to listen for this is enough. let _ = signal_user1.recv().await; - match backup_sqlite() { + match backup_sqlite().await { Ok(f) => info!("Backup to '{f}' was successful"), Err(e) => error!("Backup failed. {e:?}"), } diff --git a/src/static/scripts/admin.css b/src/static/scripts/admin.css index 1db8d4c0..ee035ac4 100644 --- a/src/static/scripts/admin.css +++ b/src/static/scripts/admin.css @@ -38,8 +38,8 @@ img { max-width: 130px; } #users-table .vw-actions, #orgs-table .vw-actions { - min-width: 130px; - max-width: 130px; + min-width: 135px; + max-width: 140px; } #users-table .vw-org-cell { max-height: 120px; diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 6a178e4b..258df5e1 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -7,6 +7,8 @@ var timeCheck = false; var ntpTimeCheck = false; var domainCheck = false; var httpsCheck = false; +var websocketCheck = false; +var httpResponseCheck = false; // ================================ // Date & Time Check @@ -76,18 +78,15 @@ async function generateSupportString(event, dj) { event.preventDefault(); event.stopPropagation(); - let supportString = "### Your environment (Generated via diagnostics page)\n"; + let supportString = "### Your environment (Generated via diagnostics page)\n\n"; supportString += `* Vaultwarden version: v${dj.current_release}\n`; supportString += `* Web-vault version: v${dj.web_vault_version}\n`; supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`; supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`; - supportString += "* Environment settings overridden: "; - if (dj.overrides != "") { - supportString += "true\n"; - } else { - supportString += "false\n"; - } + supportString += `* Database type: ${dj.db_type}\n`; + supportString += `* Database version: ${dj.db_version}\n`; + supportString += `* Environment settings overridden!: ${dj.overrides !== ""}\n`; supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`; if (dj.ip_header_exists) { supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`; @@ -99,11 +98,12 @@ async function generateSupportString(event, dj) { supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`; supportString += `* Domain Configuration Check: ${domainCheck}\n`; supportString += `* HTTPS Check: ${httpsCheck}\n`; - supportString += `* Database type: ${dj.db_type}\n`; - supportString += `* Database version: ${dj.db_version}\n`; - supportString += "* Clients used: \n"; - supportString += "* Reverse proxy and version: \n"; - supportString += "* Other relevant information: \n"; + if (dj.enable_websocket) { + supportString += `* Websocket Check: ${websocketCheck}\n`; + } else { + supportString += "* Websocket Check: disabled\n"; + } + supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`; const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, { "headers": { "Accept": "application/json" } @@ -113,10 +113,30 @@ async function generateSupportString(event, dj) { throw new Error(jsonResponse); } const configJson = await jsonResponse.json(); - supportString += "\n### Config (Generated via diagnostics page)\n
Show Running Config\n"; - supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`; - supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n
\n"; + // Start Config and Details section within a details block which is collapsed by default + supportString += "\n### Config & Details (Generated via diagnostics page)\n\n"; + supportString += "
Show Config & Details\n"; + + // Add overrides if they exists + if (dj.overrides != "") { + supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`; + } + + // Add http response check messages if they exists + if (httpResponseCheck === false) { + supportString += "\n**Failed HTTP Checks:**\n"; + // We use `innerText` here since that will convert
into new-lines + supportString += "\n```yaml\n" + document.getElementById("http-response-errors").innerText.trim() + "\n```\n"; + } + + // Add the current config in json form + supportString += "\n**Config:**\n"; + supportString += "\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n"; + + supportString += "\n
\n"; + + // Add the support string to the textbox so it can be viewed and copied document.getElementById("support-string").textContent = supportString; document.getElementById("support-string").classList.remove("d-none"); document.getElementById("copy-support").classList.remove("d-none"); @@ -199,6 +219,162 @@ function checkDns(dns_resolved) { } } +async function fetchCheckUrl(url) { + try { + const response = await fetch(url); + return { headers: response.headers, status: response.status, text: await response.text() }; + } catch (error) { + console.error(`Error fetching ${url}: ${error}`); + return { error }; + } +} + +function checkSecurityHeaders(headers, omit) { + let securityHeaders = { + "x-frame-options": ["SAMEORIGIN"], + "x-content-type-options": ["nosniff"], + "referrer-policy": ["same-origin"], + "x-xss-protection": ["0"], + "x-robots-tag": ["noindex", "nofollow"], + "content-security-policy": [ + "default-src 'self'", + "base-uri 'self'", + "form-action 'self'", + "object-src 'self' blob:", + "script-src 'self' 'wasm-unsafe-eval'", + "style-src 'self' 'unsafe-inline'", + "child-src 'self' https://*.duosecurity.com https://*.duofederal.com", + "frame-src 'self' https://*.duosecurity.com https://*.duofederal.com", + "frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh moz-extension://*", + "img-src 'self' data: https://haveibeenpwned.com", + "connect-src 'self' https://api.pwnedpasswords.com https://api.2fa.directory https://app.simplelogin.io/api/ https://app.addy.io/api/ https://api.fastmail.com/ https://api.forwardemail.net", + ] + }; + + let messages = []; + for (let header in securityHeaders) { + // Skip some headers for specific endpoints if needed + if (typeof omit === "object" && omit.includes(header) === true) { + continue; + } + // If the header exists, check if the contents matches what we expect it to be + let headerValue = headers.get(header); + if (headerValue !== null) { + securityHeaders[header].forEach((expectedValue) => { + if (headerValue.indexOf(expectedValue) === -1) { + messages.push(`'${header}' does not contain '${expectedValue}'`); + } + }); + } else { + messages.push(`'${header}' is missing!`); + } + } + return messages; +} + +async function checkHttpResponse() { + const [apiConfig, webauthnConnector, notFound, notFoundApi, badRequest, unauthorized, forbidden] = await Promise.all([ + fetchCheckUrl(`${BASE_URL}/api/config`), + fetchCheckUrl(`${BASE_URL}/webauthn-connector.html`), + fetchCheckUrl(`${BASE_URL}/admin/does-not-exist`), + fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=404`), + fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=400`), + fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=401`), + fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=403`), + ]); + + const respErrorElm = document.getElementById("http-response-errors"); + + // Check and validate the default API header responses + let apiErrors = checkSecurityHeaders(apiConfig.headers); + if (apiErrors.length >= 1) { + respErrorElm.innerHTML += "API calls:
"; + apiErrors.forEach((errMsg) => { + respErrorElm.innerHTML += `Header: ${errMsg}
`; + }); + } + + // Check the special `-connector.html` headers, these should have some headers omitted. + const omitConnectorHeaders = ["x-frame-options", "content-security-policy"]; + let connectorErrors = checkSecurityHeaders(webauthnConnector.headers, omitConnectorHeaders); + omitConnectorHeaders.forEach((header) => { + if (webauthnConnector.headers.get(header) !== null) { + connectorErrors.push(`'${header}' is present while it should not`); + } + }); + if (connectorErrors.length >= 1) { + respErrorElm.innerHTML += "2FA Connector calls:
"; + connectorErrors.forEach((errMsg) => { + respErrorElm.innerHTML += `Header: ${errMsg}
`; + }); + } + + // Check specific error code responses if they are not re-written by a reverse proxy + let responseErrors = []; + if (notFound.status !== 404 || notFound.text.indexOf("return to the web-vault") === -1) { + responseErrors.push("404 (Not Found) HTML is invalid"); + } + + if (notFoundApi.status !== 404 || notFoundApi.text.indexOf("\"message\":\"Testing error 404 response\",") === -1) { + responseErrors.push("404 (Not Found) JSON is invalid"); + } + + if (badRequest.status !== 400 || badRequest.text.indexOf("\"message\":\"Testing error 400 response\",") === -1) { + responseErrors.push("400 (Bad Request) is invalid"); + } + + if (unauthorized.status !== 401 || unauthorized.text.indexOf("\"message\":\"Testing error 401 response\",") === -1) { + responseErrors.push("401 (Unauthorized) is invalid"); + } + + if (forbidden.status !== 403 || forbidden.text.indexOf("\"message\":\"Testing error 403 response\",") === -1) { + responseErrors.push("403 (Forbidden) is invalid"); + } + + if (responseErrors.length >= 1) { + respErrorElm.innerHTML += "HTTP error responses:
"; + responseErrors.forEach((errMsg) => { + respErrorElm.innerHTML += `Response to: ${errMsg}
`; + }); + } + + if (responseErrors.length >= 1 || connectorErrors.length >= 1 || apiErrors.length >= 1) { + document.getElementById("http-response-warning").classList.remove("d-none"); + } else { + httpResponseCheck = true; + document.getElementById("http-response-success").classList.remove("d-none"); + } +} + +async function fetchWsUrl(wsUrl) { + return new Promise((resolve, reject) => { + try { + const ws = new WebSocket(wsUrl); + ws.onopen = () => { + ws.close(); + resolve(true); + }; + + ws.onerror = () => { + reject(false); + }; + } catch (_) { + reject(false); + } + }); +} + +async function checkWebsocketConnection() { + // Test Websocket connections via the anonymous (login with device) connection + const isConnected = await fetchWsUrl(`${BASE_URL}/notifications/anonymous-hub?token=admin-diagnostics`).catch(() => false); + if (isConnected) { + websocketCheck = true; + document.getElementById("websocket-success").classList.remove("d-none"); + } else { + document.getElementById("websocket-error").classList.remove("d-none"); + } +} + function init(dj) { // Time check document.getElementById("time-browser-string").textContent = browserUTC; @@ -225,6 +401,12 @@ function init(dj) { // DNS Check checkDns(dj.dns_resolved); + + checkHttpResponse(); + + if (dj.enable_websocket) { + checkWebsocketConnection(); + } } // onLoad events diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index 878e2347..195caa84 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.0.8 + * https://datatables.net/download/#bs5/dt-2.1.8 * * Included libraries: - * DataTables 2.0.8 + * DataTables 2.1.8 */ @charset "UTF-8"; @@ -45,15 +45,21 @@ table.dataTable tr.dt-hasChild td.dt-control:before { } html.dark table.dataTable td.dt-control:before, -:root[data-bs-theme=dark] table.dataTable td.dt-control:before { +:root[data-bs-theme=dark] table.dataTable td.dt-control:before, +:root[data-theme=dark] table.dataTable td.dt-control:before { border-left-color: rgba(255, 255, 255, 0.5); } html.dark table.dataTable tr.dt-hasChild td.dt-control:before, -:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before { +:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before, +:root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before { border-top-color: rgba(255, 255, 255, 0.5); border-left-color: transparent; } +div.dt-scroll { + width: 100%; +} + div.dt-scroll-body thead tr, div.dt-scroll-body tfoot tr { height: 0; @@ -377,6 +383,31 @@ table.table.dataTable.table-hover > tbody > tr.selected:hover > * { box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975); } +div.dt-container div.dt-layout-start > *:not(:last-child) { + margin-right: 1em; +} +div.dt-container div.dt-layout-end > *:not(:first-child) { + margin-left: 1em; +} +div.dt-container div.dt-layout-full { + width: 100%; +} +div.dt-container div.dt-layout-full > *:only-child { + margin-left: auto; + margin-right: auto; +} +div.dt-container div.dt-layout-table > div { + display: block !important; +} + +@media screen and (max-width: 767px) { + div.dt-container div.dt-layout-start > *:not(:last-child) { + margin-right: 0; + } + div.dt-container div.dt-layout-end > *:not(:first-child) { + margin-left: 0; + } +} div.dt-container div.dt-length label { font-weight: normal; text-align: left; @@ -400,9 +431,6 @@ div.dt-container div.dt-search input { display: inline-block; width: auto; } -div.dt-container div.dt-info { - padding-top: 0.85em; -} div.dt-container div.dt-paging { margin: 0; } diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 3d22cbde..d0361b54 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,20 +4,20 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.0.8 + * https://datatables.net/download/#bs5/dt-2.1.8 * * Included libraries: - * DataTables 2.0.8 + * DataTables 2.1.8 */ -/*! DataTables 2.0.8 +/*! DataTables 2.1.8 * © SpryMedia Ltd - datatables.net/license */ /** * @summary DataTables * @description Paginate, search and order HTML tables - * @version 2.0.8 + * @version 2.1.8 * @author SpryMedia Ltd * @contact www.datatables.net * @copyright SpryMedia Ltd. @@ -116,7 +116,6 @@ var i=0, iLen; var sId = this.getAttribute( 'id' ); - var bInitHandedOff = false; var defaults = DataTable.defaults; var $this = $(this); @@ -266,6 +265,8 @@ "rowId", "caption", "layout", + "orderDescReverse", + "typeDetect", [ "iCookieDuration", "iStateDuration" ], // backwards compat [ "oSearch", "oPreviousSearch" ], [ "aoSearchCols", "aoPreSearchCols" ], @@ -312,38 +313,14 @@ oSettings._iDisplayStart = oInit.iDisplayStart; } - /* Language definitions */ - var oLanguage = oSettings.oLanguage; - $.extend( true, oLanguage, oInit.oLanguage ); - - if ( oLanguage.sUrl ) + var defer = oInit.iDeferLoading; + if ( defer !== null ) { - /* Get the language definitions from a file - because this Ajax call makes the language - * get async to the remainder of this function we use bInitHandedOff to indicate that - * _fnInitialise will be fired by the returned Ajax handler, rather than the constructor - */ - $.ajax( { - dataType: 'json', - url: oLanguage.sUrl, - success: function ( json ) { - _fnCamelToHungarian( defaults.oLanguage, json ); - $.extend( true, oLanguage, json, oSettings.oInit.oLanguage ); + oSettings.deferLoading = true; - _fnCallbackFire( oSettings, null, 'i18n', [oSettings], true); - _fnInitialise( oSettings ); - }, - error: function () { - // Error occurred loading language file - _fnLog( oSettings, 0, 'i18n file loading error', 21 ); - - // continue on as best we can - _fnInitialise( oSettings ); - } - } ); - bInitHandedOff = true; - } - else { - _fnCallbackFire( oSettings, null, 'i18n', [oSettings]); + var tmp = Array.isArray(defer); + oSettings._iRecordsDisplay = tmp ? defer[0] : defer; + oSettings._iRecordsTotal = tmp ? defer[1] : defer; } /* @@ -410,113 +387,112 @@ } ); } - var features = oSettings.oFeatures; - var loadedInit = function () { - /* - * Sorting - * @todo For modularisation (1.11) this needs to do into a sort start up handler - */ - - // If aaSorting is not defined, then we use the first indicator in asSorting - // in case that has been altered, so the default sort reflects that option - if ( oInit.aaSorting === undefined ) { - var sorting = oSettings.aaSorting; - for ( i=0, iLen=sorting.length ; i').appendTo( $this ); - } - - caption.html( oSettings.caption ); - } - - // Store the caption side, so we can remove the element from the document - // when creating the element - if (caption.length) { - caption[0]._captionSide = caption.css('caption-side'); - oSettings.captionNode = caption[0]; - } - - if ( thead.length === 0 ) { - thead = $('').appendTo($this); - } - oSettings.nTHead = thead[0]; - $('tr', thead).addClass(oClasses.thead.row); - - var tbody = $this.children('tbody'); - if ( tbody.length === 0 ) { - tbody = $('').insertAfter(thead); - } - oSettings.nTBody = tbody[0]; - - var tfoot = $this.children('tfoot'); - if ( tfoot.length === 0 ) { - // If we are a scrolling table, and no footer has been given, then we need to create - // a tfoot element for the caption element to be appended to - tfoot = $('').appendTo($this); - } - oSettings.nTFoot = tfoot[0]; - $('tr', tfoot).addClass(oClasses.tfoot.row); - - // Check if there is data passing into the constructor - if ( oInit.aaData ) { - for ( i=0 ; i').appendTo( $this ); + } + + caption.html( oSettings.caption ); + } + + // Store the caption side, so we can remove the element from the document + // when creating the element + if (caption.length) { + caption[0]._captionSide = caption.css('caption-side'); + oSettings.captionNode = caption[0]; + } + + if ( thead.length === 0 ) { + thead = $('').appendTo($this); + } + oSettings.nTHead = thead[0]; + $('tr', thead).addClass(oClasses.thead.row); + + var tbody = $this.children('tbody'); + if ( tbody.length === 0 ) { + tbody = $('').insertAfter(thead); + } + oSettings.nTBody = tbody[0]; + + var tfoot = $this.children('tfoot'); + if ( tfoot.length === 0 ) { + // If we are a scrolling table, and no footer has been given, then we need to create + // a tfoot element for the caption element to be appended to + tfoot = $('').appendTo($this); + } + oSettings.nTFoot = tfoot[0]; + $('tr', tfoot).addClass(oClasses.tfoot.row); + + // Copy the data index array + oSettings.aiDisplay = oSettings.aiDisplayMaster.slice(); + + // Initialisation complete - table can be drawn + oSettings.bInitialised = true; + + // Language definitions + var oLanguage = oSettings.oLanguage; + $.extend( true, oLanguage, oInit.oLanguage ); + + if ( oLanguage.sUrl ) { + // Get the language definitions from a file + $.ajax( { + dataType: 'json', + url: oLanguage.sUrl, + success: function ( json ) { + _fnCamelToHungarian( defaults.oLanguage, json ); + $.extend( true, oLanguage, json, oSettings.oInit.oLanguage ); + + _fnCallbackFire( oSettings, null, 'i18n', [oSettings], true); + _fnInitialise( oSettings ); + }, + error: function () { + // Error occurred loading language file + _fnLog( oSettings, 0, 'i18n file loading error', 21 ); + + // Continue on as best we can + _fnInitialise( oSettings ); + } + } ); + } + else { + _fnCallbackFire( oSettings, null, 'i18n', [oSettings], true); + _fnInitialise( oSettings ); + } } ); _that = null; return this; @@ -563,7 +539,7 @@ * * @type string */ - builder: "bs5/dt-2.0.8", + builder: "bs5/dt-2.1.8", /** @@ -1033,6 +1009,15 @@ info: { container: 'dt-info' }, + layout: { + row: 'dt-layout-row', + cell: 'dt-layout-cell', + tableRow: 'dt-layout-table', + tableCell: '', + start: 'dt-layout-start', + end: 'dt-layout-end', + full: 'dt-layout-full' + }, length: { container: 'dt-length', select: 'dt-input' @@ -1081,7 +1066,8 @@ active: 'current', button: 'dt-paging-button', container: 'dt-paging', - disabled: 'disabled' + disabled: 'disabled', + nav: '' } } ); @@ -1156,7 +1142,7 @@ }; - var _isNumber = function ( d, decimalPoint, formatted ) { + var _isNumber = function ( d, decimalPoint, formatted, allowEmpty ) { var type = typeof d; var strType = type === 'string'; @@ -1167,7 +1153,7 @@ // If empty return immediately so there must be a number if it is a // formatted string (this stops the string "k", or "kr", etc being detected // as a formatted number for currency - if ( _empty( d ) ) { + if ( allowEmpty && _empty( d ) ) { return true; } @@ -1189,8 +1175,8 @@ }; // Is a string a number surrounded by HTML? - var _htmlNumeric = function ( d, decimalPoint, formatted ) { - if ( _empty( d ) ) { + var _htmlNumeric = function ( d, decimalPoint, formatted, allowEmpty ) { + if ( allowEmpty && _empty( d ) ) { return true; } @@ -1202,7 +1188,7 @@ var html = _isHtml( d ); return ! html ? null : - _isNumber( _stripHtml( d ), decimalPoint, formatted ) ? + _isNumber( _stripHtml( d ), decimalPoint, formatted, allowEmpty ) ? true : null; }; @@ -1244,7 +1230,7 @@ // is essential here if ( prop2 !== undefined ) { for ( ; i _max_str_len) { throw new Error('Exceeded max str len'); @@ -1340,8 +1330,11 @@ } // It is faster to just run `normalize` than it is to check if - // we need to with a regex! - var res = str.normalize("NFD"); + // we need to with a regex! (Check as it isn't available in old + // Safari) + var res = str.normalize + ? str.normalize("NFD") + : str; // Equally, here we check if a regex is needed or not return res.length !== str.length @@ -2264,6 +2257,21 @@ return a; } + /** + * Allow the result from a type detection function to be `true` while + * translating that into a string. Old type detection functions will + * return the type name if it passes. An obect store would be better, + * but not backwards compatible. + * + * @param {*} typeDetect Object or function for type detection + * @param {*} res Result from the type detection function + * @returns Type name or false + */ + function _typeResult (typeDetect, res) { + return res === true + ? typeDetect._name + : res; + } /** * Calculate the 'type' of a column @@ -2278,7 +2286,7 @@ var i, ien, j, jen, k, ken; var col, detectedType, cache; - // For each column, spin over the + // For each column, spin over the data type detection functions, seeing if one matches for ( i=0, ien=columns.length ; i col is set to and correct if needed - for (var i=0 ; i col is set to and correct if needed + for (var i=0 ; i 0 ? idx : null; + } + + // `:visible` on its own + return idx; } ); case 'name': @@ -9215,23 +9404,60 @@ } ); /** - * Set the jQuery or window object to be used by DataTables - * - * @param {*} module Library / container object - * @param {string} [type] Library or container type `lib`, `win` or `datetime`. - * If not provided, automatic detection is attempted. + * Set the libraries that DataTables uses, or the global objects. + * Note that the arguments can be either way around (legacy support) + * and the second is optional. See docs. */ - DataTable.use = function (module, type) { - if (type === 'lib' || module.fn) { + DataTable.use = function (arg1, arg2) { + // Reverse arguments for legacy support + var module = typeof arg1 === 'string' + ? arg2 + : arg1; + var type = typeof arg2 === 'string' + ? arg2 + : arg1; + + // Getter + if (module === undefined && typeof type === 'string') { + switch (type) { + case 'lib': + case 'jq': + return $; + + case 'win': + return window; + + case 'datetime': + return DataTable.DateTime; + + case 'luxon': + return __luxon; + + case 'moment': + return __moment; + + default: + return null; + } + } + + // Setter + if (type === 'lib' || type === 'jq' || (module && module.fn && module.fn.jquery)) { $ = module; } - else if (type == 'win' || module.document) { + else if (type == 'win' || (module && module.document)) { window = module; document = module.document; } - else if (type === 'datetime' || module.type === 'DateTime') { + else if (type === 'datetime' || (module && module.type === 'DateTime')) { DataTable.DateTime = module; } + else if (type === 'luxon' || (module && module.FixedOffsetZone)) { + __luxon = module; + } + else if (type === 'moment' || (module && module.isMoment)) { + __moment = module; + } } /** @@ -9487,7 +9713,7 @@ fn.call(this); } else { - this.on('init', function () { + this.on('init.dt.DT', function () { fn.call(this); }); } @@ -9640,7 +9866,7 @@ * @type string * @default Version number */ - DataTable.version = "2.0.8"; + DataTable.version = "2.1.8"; /** * Private data store, containing all of the settings objects that are @@ -10485,7 +10711,8 @@ first: 'First', last: 'Last', next: 'Next', - previous: 'Previous' + previous: 'Previous', + number: '' } }, @@ -10665,6 +10892,10 @@ }, + /** The initial data order is reversed when `desc` ordering */ + orderDescReverse: true, + + /** * This parameter allows you to have define the global filtering state at * initialisation time. As an object the `search` parameter must be @@ -10713,7 +10944,7 @@ * * `full_numbers` - 'First', 'Previous', 'Next' and 'Last' buttons, plus page numbers * * `first_last_numbers` - 'First' and 'Last' buttons, plus page numbers */ - "sPaginationType": "full_numbers", + "sPaginationType": "", /** @@ -10783,7 +11014,13 @@ /** * Caption value */ - "caption": null + "caption": null, + + + /** + * For server-side processing - use the data from the DOM for the first draw + */ + iDeferLoading: null }; _fnHungarianMap( DataTable.defaults ); @@ -11726,7 +11963,13 @@ captionNode: null, - colgroup: null + colgroup: null, + + /** Delay loading of data */ + deferLoading: null, + + /** Allow auto type detection */ + typeDetect: true }; /** @@ -11750,7 +11993,7 @@ }, full: function () { - return [ 'first', 'previous', 'next', 'last' ]; + return [ 'first', 'previous', 'next', 'last' ]; }, numbers: function () { @@ -11764,11 +12007,11 @@ full_numbers: function () { return [ 'first', 'previous', 'numbers', 'next', 'last' ]; }, - + first_last: function () { return ['first', 'last']; }, - + first_last_numbers: function () { return ['first', 'numbers', 'last']; }, @@ -11850,38 +12093,56 @@ * to make working with DataTables a little bit easier. */ - function __mldFnName(name) { - return name.replace(/[\W]/g, '_') - } - - // Common logic for moment, luxon or a date action - function __mld( dt, momentFn, luxonFn, dateFn, arg1 ) { - if (window.moment) { - return dt[momentFn]( arg1 ); + /** + * Common logic for moment, luxon or a date action. + * + * Happens after __mldObj, so don't need to call `resolveWindowsLibs` again + */ + function __mld( dtLib, momentFn, luxonFn, dateFn, arg1 ) { + if (__moment) { + return dtLib[momentFn]( arg1 ); } - else if (window.luxon) { - return dt[luxonFn]( arg1 ); + else if (__luxon) { + return dtLib[luxonFn]( arg1 ); } - return dateFn ? dt[dateFn]( arg1 ) : dt; + return dateFn ? dtLib[dateFn]( arg1 ) : dtLib; } var __mlWarning = false; + var __luxon; // Can be assigned in DateTeble.use() + var __moment; // Can be assigned in DateTeble.use() + + /** + * + */ + function resolveWindowLibs() { + if (window.luxon && ! __luxon) { + __luxon = window.luxon; + } + + if (window.moment && ! __moment) { + __moment = window.moment; + } + } + function __mldObj (d, format, locale) { var dt; - if (window.moment) { - dt = window.moment.utc( d, format, locale, true ); + resolveWindowLibs(); + + if (__moment) { + dt = __moment.utc( d, format, locale, true ); if (! dt.isValid()) { return null; } } - else if (window.luxon) { + else if (__luxon) { dt = format && typeof d === 'string' - ? window.luxon.DateTime.fromFormat( d, format ) - : window.luxon.DateTime.fromISO( d ); + ? __luxon.DateTime.fromFormat( d, format ) + : __luxon.DateTime.fromISO( d ); if (! dt.isValid) { return null; @@ -11926,11 +12187,11 @@ from = null; } - var typeName = 'datetime' + (to ? '-' + __mldFnName(to) : ''); + var typeName = 'datetime' + (to ? '-' + to : ''); // Add type detection and sorting specific to this date format - we need to be able to identify // date type columns as such, rather than as numbers in extensions. Hence the need for this. - if (! DataTable.ext.type.order[typeName]) { + if (! DataTable.ext.type.order[typeName + '-pre']) { DataTable.type(typeName, { detect: function (d) { // The renderer will give the value to type detect as the type! @@ -12029,7 +12290,7 @@ // Formatted date time detection - use by declaring the formats you are going to use DataTable.datetime = function ( format, locale ) { - var typeName = 'datetime-detect-' + __mldFnName(format); + var typeName = 'datetime-' + format; if (! locale) { locale = 'en'; @@ -12169,7 +12430,7 @@ return { className: _extTypes.className[name], detect: _extTypes.detect.find(function (fn) { - return fn.name === name; + return fn._name === name; }), order: { pre: _extTypes.order[name + '-pre'], @@ -12184,27 +12445,20 @@ var setProp = function(prop, propVal) { _extTypes[prop][name] = propVal; }; - var setDetect = function (fn) { - // Wrap to allow the function to return `true` rather than - // specifying the type name. - var cb = function (d, s) { - var ret = fn(d, s); + var setDetect = function (detect) { + // `detect` can be a function or an object - we set a name + // property for either - that is used for the detection + Object.defineProperty(detect, "_name", {value: name}); - return ret === true - ? name - : ret; - }; - Object.defineProperty(cb, "name", {value: name}); - - var idx = _extTypes.detect.findIndex(function (fn) { - return fn.name === name; + var idx = _extTypes.detect.findIndex(function (item) { + return item._name === name; }); if (idx === -1) { - _extTypes.detect.unshift(cb); + _extTypes.detect.unshift(detect); } else { - _extTypes.detect.splice(idx, 1, cb); + _extTypes.detect.splice(idx, 1, detect); } }; var setOrder = function (obj) { @@ -12260,10 +12514,23 @@ // Get a list of types DataTable.types = function () { return _extTypes.detect.map(function (fn) { - return fn.name; + return fn._name; }); }; + var __diacriticSort = function (a, b) { + a = a !== null && a !== undefined ? a.toString().toLowerCase() : ''; + b = b !== null && b !== undefined ? b.toString().toLowerCase() : ''; + + // Checked for `navigator.languages` support in `oneOf` so this code can't execute in old + // Safari and thus can disable this check + // eslint-disable-next-line compat/compat + return a.localeCompare(b, navigator.languages[0] || navigator.language, { + numeric: true, + ignorePunctuation: true, + }); + } + // // Built in data types // @@ -12276,7 +12543,7 @@ pre: function ( a ) { // This is a little complex, but faster than always calling toString, // http://jsperf.com/tostring-v-check - return _empty(a) ? + return _empty(a) && typeof a !== 'boolean' ? '' : typeof a === 'string' ? a.toLowerCase() : @@ -12288,11 +12555,38 @@ search: _filterString(false, true) }); + DataTable.type('string-utf8', { + detect: { + allOf: function ( d ) { + return true; + }, + oneOf: function ( d ) { + // At least one data point must contain a non-ASCII character + // This line will also check if navigator.languages is supported or not. If not (Safari 10.0-) + // this data type won't be supported. + // eslint-disable-next-line compat/compat + return ! _empty( d ) && navigator.languages && typeof d === 'string' && d.match(/[^\x00-\x7F]/); + } + }, + order: { + asc: __diacriticSort, + desc: function (a, b) { + return __diacriticSort(a, b) * -1; + } + }, + search: _filterString(false, true) + }); + DataTable.type('html', { - detect: function ( d ) { - return _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1) ? - 'html' : null; + detect: { + allOf: function ( d ) { + return _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1); + }, + oneOf: function ( d ) { + // At least one data point must contain a `<` + return ! _empty( d ) && typeof d === 'string' && d.indexOf('<') !== -1; + } }, order: { pre: function ( a ) { @@ -12309,16 +12603,21 @@ DataTable.type('date', { className: 'dt-type-date', - detect: function ( d ) - { - // V8 tries _very_ hard to make a string passed into `Date.parse()` - // valid, so we need to use a regex to restrict date formats. Use a - // plug-in for anything other than ISO8601 style strings - if ( d && !(d instanceof Date) && ! _re_date.test(d) ) { - return null; + detect: { + allOf: function ( d ) { + // V8 tries _very_ hard to make a string passed into `Date.parse()` + // valid, so we need to use a regex to restrict date formats. Use a + // plug-in for anything other than ISO8601 style strings + if ( d && !(d instanceof Date) && ! _re_date.test(d) ) { + return null; + } + var parsed = Date.parse(d); + return (parsed !== null && !isNaN(parsed)) || _empty(d); + }, + oneOf: function ( d ) { + // At least one entry must be a date or a string with a date + return (d instanceof Date) || (typeof d === 'string' && _re_date.test(d)); } - var parsed = Date.parse(d); - return (parsed !== null && !isNaN(parsed)) || _empty(d) ? 'date' : null; }, order: { pre: function ( d ) { @@ -12331,10 +12630,16 @@ DataTable.type('html-num-fmt', { className: 'dt-type-numeric', - detect: function ( d, settings ) - { - var decimal = settings.oLanguage.sDecimal; - return _htmlNumeric( d, decimal, true ) ? 'html-num-fmt' : null; + detect: { + allOf: function ( d, settings ) { + var decimal = settings.oLanguage.sDecimal; + return _htmlNumeric( d, decimal, true, false ); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.oLanguage.sDecimal; + return _htmlNumeric( d, decimal, true, false ); + } }, order: { pre: function ( d, s ) { @@ -12348,10 +12653,16 @@ DataTable.type('html-num', { className: 'dt-type-numeric', - detect: function ( d, settings ) - { - var decimal = settings.oLanguage.sDecimal; - return _htmlNumeric( d, decimal ) ? 'html-num' : null; + detect: { + allOf: function ( d, settings ) { + var decimal = settings.oLanguage.sDecimal; + return _htmlNumeric( d, decimal, false, true ); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.oLanguage.sDecimal; + return _htmlNumeric( d, decimal, false, false ); + } }, order: { pre: function ( d, s ) { @@ -12365,10 +12676,16 @@ DataTable.type('num-fmt', { className: 'dt-type-numeric', - detect: function ( d, settings ) - { - var decimal = settings.oLanguage.sDecimal; - return _isNumber( d, decimal, true ) ? 'num-fmt' : null; + detect: { + allOf: function ( d, settings ) { + var decimal = settings.oLanguage.sDecimal; + return _isNumber( d, decimal, true, true ); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.oLanguage.sDecimal; + return _isNumber( d, decimal, true, false ); + } }, order: { pre: function ( d, s ) { @@ -12381,10 +12698,16 @@ DataTable.type('num', { className: 'dt-type-numeric', - detect: function ( d, settings ) - { - var decimal = settings.oLanguage.sDecimal; - return _isNumber( d, decimal ) ? 'num' : null; + detect: { + allOf: function ( d, settings ) { + var decimal = settings.oLanguage.sDecimal; + return _isNumber( d, decimal, false, true ); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.oLanguage.sDecimal; + return _isNumber( d, decimal, false, false ); + } }, order: { pre: function (d, s) { @@ -12468,11 +12791,18 @@ // `DT` namespace will allow the event to be removed automatically // on destroy, while the `dt` namespaced event is the one we are // listening for - $(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting ) { + $(settings.nTable).on( 'order.dt.DT column-visibility.dt.DT', function ( e, ctx ) { if ( settings !== ctx ) { // need to check this this is the host return; // table, not a nested one } + var sorting = ctx.sortDetails; + + if (! sorting) { + return; + } + + var i; var orderClasses = classes.order; var columns = ctx.api.columns( cell ); var col = settings.aoColumns[columns.flatten()[0]]; @@ -12480,9 +12810,7 @@ var ariaType = ''; var indexes = columns.indexes(); var sortDirs = columns.orderable(true).flatten(); - var orderedColumns = ',' + sorting.map( function (val) { - return val.col; - } ).join(',') + ','; + var orderedColumns = _pluck(sorting, 'col'); cell .removeClass( @@ -12492,10 +12820,18 @@ .toggleClass( orderClasses.none, ! orderable ) .toggleClass( orderClasses.canAsc, orderable && sortDirs.includes('asc') ) .toggleClass( orderClasses.canDesc, orderable && sortDirs.includes('desc') ); - - var sortIdx = orderedColumns.indexOf( ',' + indexes.toArray().join(',') + ',' ); - if ( sortIdx !== -1 ) { + // Determine if all of the columns that this cell covers are included in the + // current ordering + var isOrdering = true; + + for (i=0; i') - .addClass('dt-layout-row') + .attr('id', items.id || null) + .addClass(items.className || classes.row) .appendTo( container ); $.each( items, function (key, val) { - var klass = ! val.table ? - 'dt-'+key+' ' : - ''; + if (key === 'id' || key === 'className') { + return; + } + + var klass = ''; if (val.table) { - row.addClass('dt-layout-table'); + row.addClass(classes.tableRow); + klass += classes.tableCell + ' '; + } + + if (key === 'start') { + klass += classes.start; + } + else if (key === 'end') { + klass += classes.end; + } + else { + klass += classes.full; } $('
') .attr({ id: val.id || null, - "class": 'dt-layout-cell '+klass+(val.className || '') + "class": val.className + ? val.className + : classes.cell + ' ' + klass }) .append( val.contents ) .appendTo( row ); @@ -12576,6 +12941,25 @@ } }; + function _divProp(el, prop, val) { + if (val) { + el[prop] = val; + } + } + + DataTable.feature.register( 'div', function ( settings, opts ) { + var n = $('
')[0]; + + if (opts) { + _divProp(n, 'className', opts.className); + _divProp(n, 'id', opts.id); + _divProp(n, 'innerHTML', opts.html); + _divProp(n, 'textContent', opts.text); + } + + return n; + } ); + DataTable.feature.register( 'info', function ( settings, opts ) { // For compatibility with the legacy `info` top level option if (! settings.oFeatures.bInfo) { @@ -12675,6 +13059,7 @@ opts = $.extend({ placeholder: language.sSearchPlaceholder, + processing: false, text: language.sSearch }, opts); @@ -12718,13 +13103,15 @@ /* Now do the filter */ if ( val != previousSearch.search ) { - previousSearch.search = val; - - _fnFilterComplete( settings, previousSearch ); - - // Need to redraw, without resorting - settings._iDisplayStart = 0; - _fnDraw( settings ); + _fnProcessingRun(settings, opts.processing, function () { + previousSearch.search = val; + + _fnFilterComplete( settings, previousSearch ); + + // Need to redraw, without resorting + settings._iDisplayStart = 0; + _fnDraw( settings ); + }); } }; @@ -12782,17 +13169,21 @@ opts = $.extend({ buttons: DataTable.ext.pager.numbers_length, type: settings.sPaginationType, - boundaryNumbers: true + boundaryNumbers: true, + firstLast: true, + previousNext: true, + numbers: true }, opts); - // To be removed in 2.1 - if (opts.numbers) { - opts.buttons = opts.numbers; - } - - var host = $('
').addClass( settings.oClasses.paging.container + ' paging_' + opts.type ); + var host = $('
') + .addClass(settings.oClasses.paging.container + (opts.type ? ' paging_' + opts.type : '')) + .append( + $('