Browse Source

Merge remote-tracking branch 'dani/main' into sso-support

pull/3899/head
Timshel 2 weeks ago
parent
commit
8d91f351a3
  1. 8
      .github/ISSUE_TEMPLATE/bug_report.yml
  2. 2
      .github/workflows/trivy.yml
  3. 185
      Cargo.lock
  4. 18
      Cargo.toml
  5. 1
      src/api/admin.rs
  6. 103
      src/api/core/accounts.rs
  7. 20
      src/api/core/ciphers.rs
  8. 2
      src/api/core/mod.rs
  9. 9
      src/api/core/organizations.rs
  10. 2
      src/api/web.rs
  11. 25
      src/config.rs
  12. 24
      src/db/models/cipher.rs
  13. 8
      src/db/models/collection.rs

8
.github/ISSUE_TEMPLATE/bug_report.yml

@ -20,17 +20,17 @@ body:
See here [how to enable the admin page](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page).
> [!IMPORTANT]
> :bangbang: Search for existing **Open _AND_ Closed** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=) regarding your topic before posting!
> ## :bangbang: Search for existing **Closed _AND_ Open** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=) regarding your topic before posting! :bangbang:
#
- type: checkboxes
id: checklist
attributes:
label: Prerequisites
description: Please confirm you have completed the following before submitting an issue
description: Please confirm you have completed the following before submitting an issue!
options:
- label: I have searched the existing issues and discussions
- label: I have searched the existing **Closed _AND_ Open** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=)
required: true
- label: I have read the documentation
- label: I have searched and read the [documentation](https://github.com/dani-garcia/vaultwarden/wiki/)
required: true
#
- id: support-string

2
.github/workflows/trivy.yml

@ -48,6 +48,6 @@ jobs:
severity: CRITICAL,HIGH
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
with:
sarif_file: 'trivy-results.sarif'

185
Cargo.lock

@ -173,9 +173,9 @@ dependencies = [
[[package]]
name = "async-io"
version = "2.4.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3"
checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca"
dependencies = [
"async-lock",
"cfg-if",
@ -186,8 +186,7 @@ dependencies = [
"polling",
"rustix",
"slab",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@ -203,9 +202,9 @@ dependencies = [
[[package]]
name = "async-process"
version = "2.3.1"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc"
checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00"
dependencies = [
"async-channel 2.5.0",
"async-io",
@ -217,14 +216,13 @@ dependencies = [
"event-listener 5.4.0",
"futures-lite",
"rustix",
"tracing",
]
[[package]]
name = "async-signal"
version = "0.2.11"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d"
checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1"
dependencies = [
"async-io",
"async-lock",
@ -235,7 +233,7 @@ dependencies = [
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@ -333,9 +331,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-config"
version = "1.8.1"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c18d005c70d2b9c0c1ea8876c039db0ec7fb71164d25c73ccea21bf41fd02171"
checksum = "c0baa720ebadea158c5bda642ac444a2af0cdf7bb66b46d1e4533de5d1f449d0"
dependencies = [
"aws-credential-types",
"aws-runtime",
@ -363,9 +361,9 @@ dependencies = [
[[package]]
name = "aws-credential-types"
version = "1.2.3"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "687bc16bc431a8533fe0097c7f0182874767f920989d7260950172ae8e3c4465"
checksum = "b68c2194a190e1efc999612792e25b1ab3abfefe4306494efaaabc25933c0cbe"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@ -375,9 +373,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.5.8"
version = "1.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f6c68419d8ba16d9a7463671593c54f81ba58cab466e9b759418da606dcc2e2"
checksum = "b2090e664216c78e766b6bac10fe74d2f451c02441d43484cd76ac9a295075f7"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@ -399,9 +397,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.74.0"
version = "1.78.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a69de9c1b9272da2872af60c7402683e7f45c06267735b4332deacb203239b"
checksum = "dbd7bc4bd34303733bded362c4c997a39130eac4310257c79aae8484b1c4b724"
dependencies = [
"aws-credential-types",
"aws-runtime",
@ -421,9 +419,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.75.0"
version = "1.79.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b161d836fac72bdd5ac1a4cd1cdc38ab888c7af26cfd95f661be4409505e63"
checksum = "77358d25f781bb106c1a69531231d4fd12c6be904edb0c47198c604df5a2dbca"
dependencies = [
"aws-credential-types",
"aws-runtime",
@ -443,9 +441,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.76.0"
version = "1.80.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb1cd79a3412751a341a28e2cd0d6fa4345241976da427b075a0c0cd5409f886"
checksum = "06e3ed2a9b828ae7763ddaed41d51724d2661a50c45f845b08967e52f4939cfc"
dependencies = [
"aws-credential-types",
"aws-runtime",
@ -499,9 +497,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http"
version = "0.62.1"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99335bec6cdc50a346fda1437f9fefe33abf8c99060739a546a16457f2862ca9"
checksum = "43c82ba4cab184ea61f6edaafc1072aad3c2a17dcf4c0fce19ac5694b90d8b5f"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
@ -547,9 +545,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.8.4"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3aaec682eb189e43c8a19c3dab2fe54590ad5f2cc2d26ab27608a20f2acf81c"
checksum = "660f70d9d8af6876b4c9aa8dcb0dbaf0f89b04ee9a4455bea1b4ba03b15f26f6"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@ -570,9 +568,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.8.3"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9852b9226cb60b78ce9369022c0df678af1cac231c882d5da97a0c4e03be6e67"
checksum = "937a49ecf061895fca4a6dd8e864208ed9be7546c0527d04bc07d502ec5fba1c"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@ -619,9 +617,9 @@ dependencies = [
[[package]]
name = "aws-types"
version = "1.3.7"
version = "1.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a322fec39e4df22777ed3ad8ea868ac2f94cd15e1a55f6ee8d8d6305057689a"
checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
@ -825,16 +823,16 @@ dependencies = [
[[package]]
name = "cached"
version = "0.55.1"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0839c297f8783316fcca9d90344424e968395413f0662a5481f79c6648bbc14"
checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c"
dependencies = [
"ahash",
"async-trait",
"cached_proc_macro",
"cached_proc_macro_types",
"futures",
"hashbrown 0.14.5",
"hashbrown 0.15.4",
"once_cell",
"thiserror 2.0.12",
"tokio",
@ -843,9 +841,9 @@ dependencies = [
[[package]]
name = "cached_proc_macro"
version = "0.24.0"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673992d934f0711b68ebb3e1b79cdc4be31634b37c98f26867ced0438ca5c603"
checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201"
dependencies = [
"darling",
"proc-macro2",
@ -901,9 +899,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.29"
version = "1.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
dependencies = [
"jobserver",
"libc",
@ -1982,7 +1980,7 @@ dependencies = [
"parking_lot",
"portable-atomic",
"quanta",
"rand 0.9.1",
"rand 0.9.2",
"smallvec",
"spinning_top",
"web-time",
@ -2115,7 +2113,7 @@ dependencies = [
"idna",
"ipnet",
"once_cell",
"rand 0.9.1",
"rand 0.9.2",
"ring",
"thiserror 2.0.12",
"tinyvec",
@ -2137,7 +2135,7 @@ dependencies = [
"moka",
"once_cell",
"parking_lot",
"rand 0.9.1",
"rand 0.9.2",
"resolv-conf",
"smallvec",
"thiserror 2.0.12",
@ -2276,7 +2274,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@ -2312,7 +2310,7 @@ dependencies = [
"http 1.3.1",
"hyper 1.6.0",
"hyper-util",
"rustls 0.23.29",
"rustls 0.23.30",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
@ -2339,9 +2337,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.15"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df"
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
dependencies = [
"base64 0.22.1",
"bytes",
@ -2355,7 +2353,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"socket2 0.6.0",
"system-configuration",
"tokio",
"tower-service",
@ -2540,9 +2538,9 @@ dependencies = [
[[package]]
name = "io-uring"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags",
"cfg-if",
@ -2555,7 +2553,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2",
"socket2 0.5.10",
"widestring",
"windows-sys 0.48.0",
"winreg",
@ -2684,9 +2682,9 @@ dependencies = [
[[package]]
name = "lettre"
version = "0.11.17"
version = "0.11.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb2a0354e9ece2fcdcf9fa53417f6de587230c0c248068eb058fa26c4a753179"
checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56"
dependencies = [
"async-std",
"async-trait",
@ -2704,10 +2702,10 @@ dependencies = [
"nom 8.0.0",
"percent-encoding",
"quoted_printable",
"rustls 0.23.29",
"rustls 0.23.30",
"rustls-native-certs",
"serde",
"socket2",
"socket2 0.6.0",
"tokio",
"tokio-rustls 0.26.2",
"tracing",
@ -2761,9 +2759,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "litrs"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]]
name = "lock_api"
@ -3178,12 +3176,11 @@ dependencies = [
[[package]]
name = "opendal"
version = "0.53.3"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f947c4efbca344c1a125753366033c8107f552b2e3f8251815ed1908f116ca3e"
checksum = "ffb9838d0575c6dbaf3fcec7255af8d5771996d4af900bbb6fa9a314dec00a1a"
dependencies = [
"anyhow",
"async-trait",
"backon",
"base64 0.22.1",
"bytes",
@ -3634,17 +3631,16 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "polling"
version = "3.8.0"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@ -3797,8 +3793,8 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls 0.23.29",
"socket2",
"rustls 0.23.30",
"socket2 0.5.10",
"thiserror 2.0.12",
"tokio",
"tracing",
@ -3814,10 +3810,10 @@ dependencies = [
"bytes",
"getrandom 0.3.3",
"lru-slab",
"rand 0.9.1",
"rand 0.9.2",
"ring",
"rustc-hash",
"rustls 0.23.29",
"rustls 0.23.30",
"rustls-pki-types",
"slab",
"thiserror 2.0.12",
@ -3835,7 +3831,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"socket2 0.5.10",
"tracing",
"windows-sys 0.59.0",
]
@ -3885,9 +3881,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
@ -3942,9 +3938,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.13"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags",
]
@ -4093,7 +4089,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.29",
"rustls 0.23.30",
"rustls-native-certs",
"rustls-pki-types",
"serde",
@ -4315,9 +4311,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.25"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc-hash"
@ -4361,9 +4357,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.29"
version = "0.23.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
checksum = "069a8df149a16b1a12dcc31497c3396a173844be3cac4bd40c9e7671fef96671"
dependencies = [
"log",
"once_cell",
@ -4632,9 +4628,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
dependencies = [
"itoa",
"memchr",
@ -4844,6 +4840,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "spin"
version = "0.9.8"
@ -5142,9 +5148,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.46.1"
version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
dependencies = [
"backtrace",
"bytes",
@ -5155,9 +5161,9 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2",
"socket2 0.6.0",
"tokio-macros",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -5197,7 +5203,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
dependencies = [
"rustls 0.23.29",
"rustls 0.23.30",
"tokio",
]
@ -5628,7 +5634,7 @@ dependencies = [
"pastey",
"percent-encoding",
"pico-args",
"rand 0.9.1",
"rand 0.9.2",
"regex",
"reqsign",
"reqwest",
@ -5832,9 +5838,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502"
checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
dependencies = [
"rustls-pki-types",
]
@ -6042,7 +6048,7 @@ version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.2",
"windows-targets 0.53.3",
]
[[package]]
@ -6078,10 +6084,11 @@ dependencies = [
[[package]]
name = "windows-targets"
version = "0.53.2"
version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
@ -6343,7 +6350,7 @@ dependencies = [
"form_urlencoded",
"futures",
"hmac",
"rand 0.9.1",
"rand 0.9.2",
"reqwest",
"sha1",
"threadpool",

18
Cargo.toml

@ -77,12 +77,12 @@ dashmap = "6.1.0"
# Async futures
futures = "0.3.31"
tokio = { version = "1.46.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
tokio = { version = "1.47.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
tokio-util = { version = "0.7.15", features = ["compat"]}
# A generic serialization/deserialization framework
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
serde_json = "1.0.141"
# A safe, extensible ORM and Query builder
diesel = { version = "2.2.12", features = ["chrono", "r2d2", "numeric"] }
@ -96,7 +96,7 @@ diesel-derive-newtype = "2.1.2"
libsqlite3-sys = { version = "0.35.0", features = ["bundled"], optional = true }
# Crypto-related libraries
rand = "0.9.1"
rand = "0.9.2"
ring = "0.17.14"
subtle = "2.6.1"
@ -130,7 +130,7 @@ webauthn-rs = "0.3.2"
url = "2.5.4"
# Email libraries
lettre = { version = "0.11.17", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
lettre = { version = "0.11.18", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
email_address = "0.2.9"
@ -149,7 +149,7 @@ bytes = "1.10.1"
svg-hush = "0.9.5"
# Cache function results (Used for version check and favicon fetching)
cached = { version = "0.55.1", features = ["async"] }
cached = { version = "0.56.0", features = ["async"] }
# Used for custom short lived cookie jar during favicon extraction
cookie = "0.18.1"
@ -188,13 +188,13 @@ rpassword = "7.4.0"
grass_compiler = { version = "0.13.4", default-features = false }
# File are accessed through Apache OpenDAL
opendal = { version = "0.53.3", features = ["services-fs"], default-features = false }
opendal = { version = "0.54.0", features = ["services-fs"], default-features = false }
# For retrieving AWS credentials, including temporary SSO credentials
anyhow = { version = "1.0.98", optional = true }
aws-config = { version = "1.8.1", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
aws-credential-types = { version = "1.2.3", optional = true }
aws-smithy-runtime-api = { version = "1.8.3", optional = true }
aws-config = { version = "1.8.3", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
aws-credential-types = { version = "1.2.4", optional = true }
aws-smithy-runtime-api = { version = "1.8.5", optional = true }
http = { version = "1.3.1", optional = true }
reqsign = { version = "0.16.5", optional = true }

1
src/api/admin.rs

@ -640,6 +640,7 @@ use cached::proc_macro::cached;
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already
/// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit
/// Any cache will be lost if Vaultwarden is restarted
use std::time::Duration; // Needed for cached
#[cached(time = 600, sync_writes = "default")]
async fn get_release_info(has_http_access: bool) -> (String, String, String) {
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.

103
src/api/core/accounts.rs

@ -639,14 +639,45 @@ use super::sends::{update_send_from_data, SendData};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct KeyData {
account_unlock_data: RotateAccountUnlockData,
account_keys: RotateAccountKeys,
account_data: RotateAccountData,
old_master_key_authentication_hash: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RotateAccountUnlockData {
emergency_access_unlock_data: Vec<UpdateEmergencyAccessData>,
master_password_unlock_data: MasterPasswordUnlockData,
organization_account_recovery_unlock_data: Vec<UpdateResetPasswordData>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct MasterPasswordUnlockData {
kdf_type: i32,
kdf_iterations: i32,
kdf_parallelism: Option<i32>,
kdf_memory: Option<i32>,
email: String,
master_key_authentication_hash: String,
master_key_encrypted_user_key: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RotateAccountKeys {
user_key_encrypted_account_private_key: String,
account_public_key: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RotateAccountData {
ciphers: Vec<CipherData>,
folders: Vec<UpdateFolderData>,
sends: Vec<SendData>,
emergency_access_keys: Vec<UpdateEmergencyAccessData>,
reset_password_keys: Vec<UpdateResetPasswordData>,
key: String,
master_password_hash: String,
private_key: String,
}
fn validate_keydata(
@ -656,10 +687,24 @@ fn validate_keydata(
existing_emergency_access: &[EmergencyAccess],
existing_memberships: &[Membership],
existing_sends: &[Send],
user: &User,
) -> EmptyResult {
if user.client_kdf_type != data.account_unlock_data.master_password_unlock_data.kdf_type
|| user.client_kdf_iter != data.account_unlock_data.master_password_unlock_data.kdf_iterations
|| user.client_kdf_memory != data.account_unlock_data.master_password_unlock_data.kdf_memory
|| user.client_kdf_parallelism != data.account_unlock_data.master_password_unlock_data.kdf_parallelism
|| user.email != data.account_unlock_data.master_password_unlock_data.email
{
err!("Changing the kdf variant or email is not supported during key rotation");
}
if user.public_key.as_ref() != Some(&data.account_keys.account_public_key) {
err!("Changing the asymmetric keypair is not possible during key rotation")
}
// Check that we're correctly rotating all the user's ciphers
let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::<HashSet<&CipherId>>();
let provided_cipher_ids = data
.account_data
.ciphers
.iter()
.filter(|c| c.organization_id.is_none())
@ -671,7 +716,8 @@ fn validate_keydata(
// Check that we're correctly rotating all the user's folders
let existing_folder_ids = existing_folders.iter().map(|f| &f.uuid).collect::<HashSet<&FolderId>>();
let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_ref()).collect::<HashSet<&FolderId>>();
let provided_folder_ids =
data.account_data.folders.iter().filter_map(|f| f.id.as_ref()).collect::<HashSet<&FolderId>>();
if !provided_folder_ids.is_superset(&existing_folder_ids) {
err!("All existing folders must be included in the rotation")
}
@ -679,8 +725,12 @@ fn validate_keydata(
// Check that we're correctly rotating all the user's emergency access keys
let existing_emergency_access_ids =
existing_emergency_access.iter().map(|ea| &ea.uuid).collect::<HashSet<&EmergencyAccessId>>();
let provided_emergency_access_ids =
data.emergency_access_keys.iter().map(|ea| &ea.id).collect::<HashSet<&EmergencyAccessId>>();
let provided_emergency_access_ids = data
.account_unlock_data
.emergency_access_unlock_data
.iter()
.map(|ea| &ea.id)
.collect::<HashSet<&EmergencyAccessId>>();
if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) {
err!("All existing emergency access keys must be included in the rotation")
}
@ -688,15 +738,19 @@ fn validate_keydata(
// Check that we're correctly rotating all the user's reset password keys
let existing_reset_password_ids =
existing_memberships.iter().map(|m| &m.org_uuid).collect::<HashSet<&OrganizationId>>();
let provided_reset_password_ids =
data.reset_password_keys.iter().map(|rp| &rp.organization_id).collect::<HashSet<&OrganizationId>>();
let provided_reset_password_ids = data
.account_unlock_data
.organization_account_recovery_unlock_data
.iter()
.map(|rp| &rp.organization_id)
.collect::<HashSet<&OrganizationId>>();
if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) {
err!("All existing reset password keys must be included in the rotation")
}
// Check that we're correctly rotating all the user's sends
let existing_send_ids = existing_sends.iter().map(|s| &s.uuid).collect::<HashSet<&SendId>>();
let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_ref()).collect::<HashSet<&SendId>>();
let provided_send_ids = data.account_data.sends.iter().filter_map(|s| s.id.as_ref()).collect::<HashSet<&SendId>>();
if !provided_send_ids.is_superset(&existing_send_ids) {
err!("All existing sends must be included in the rotation")
}
@ -704,12 +758,12 @@ fn validate_keydata(
Ok(())
}
#[post("/accounts/key", data = "<data>")]
#[post("/accounts/key-management/rotate-user-account-keys", data = "<data>")]
async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
// TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything.
let data: KeyData = data.into_inner();
if !headers.user.check_valid_password(&data.master_password_hash) {
if !headers.user.check_valid_password(&data.old_master_key_authentication_hash) {
err!("Invalid password")
}
@ -717,7 +771,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Bitwarden does not process the import if there is one item invalid.
// Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
Cipher::validate_cipher_data(&data.ciphers)?;
Cipher::validate_cipher_data(&data.account_data.ciphers)?;
let user_id = &headers.user.uuid;
@ -738,10 +792,11 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
&existing_emergency_access,
&existing_memberships,
&existing_sends,
&headers.user,
)?;
// Update folder data
for folder_data in data.folders {
for folder_data in data.account_data.folders {
// Skip `null` folder id entries.
// See: https://github.com/bitwarden/clients/issues/8453
if let Some(folder_id) = folder_data.id {
@ -755,7 +810,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
}
// Update emergency access data
for emergency_access_data in data.emergency_access_keys {
for emergency_access_data in data.account_unlock_data.emergency_access_unlock_data {
let Some(saved_emergency_access) =
existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id)
else {
@ -767,7 +822,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
}
// Update reset password data
for reset_password_data in data.reset_password_keys {
for reset_password_data in data.account_unlock_data.organization_account_recovery_unlock_data {
let Some(membership) =
existing_memberships.iter_mut().find(|m| m.org_uuid == reset_password_data.organization_id)
else {
@ -779,7 +834,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
}
// Update send data
for send_data in data.sends {
for send_data in data.account_data.sends {
let Some(send) = existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) else {
err!("Send doesn't exist")
};
@ -790,7 +845,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Update cipher data
use super::ciphers::update_cipher_from_data;
for cipher_data in data.ciphers {
for cipher_data in data.account_data.ciphers {
if cipher_data.organization_id.is_none() {
let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap())
else {
@ -807,9 +862,13 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Update user data
let mut user = headers.user;
user.akey = data.key;
user.private_key = Some(data.private_key);
user.reset_security_stamp();
user.private_key = Some(data.account_keys.user_key_encrypted_account_private_key);
user.set_password(
&data.account_unlock_data.master_password_unlock_data.master_key_authentication_hash,
Some(data.account_unlock_data.master_password_unlock_data.master_key_encrypted_user_key),
true,
None,
);
let save_result = user.save(&mut conn).await;

20
src/api/core/ciphers.rs

@ -1924,11 +1924,21 @@ impl CipherSyncData {
// Generate a HashMap with the collections_uuid as key and the CollectionGroup record
let user_collections_groups: HashMap<CollectionId, CollectionGroup> = if CONFIG.org_groups_enabled() {
CollectionGroup::find_by_user(user_id, conn)
.await
.into_iter()
.map(|collection_group| (collection_group.collections_uuid.clone(), collection_group))
.collect()
CollectionGroup::find_by_user(user_id, conn).await.into_iter().fold(
HashMap::new(),
|mut combined_permissions, cg| {
combined_permissions
.entry(cg.collections_uuid.clone())
.and_modify(|existing| {
// Combine permissions: take the most permissive settings.
existing.read_only &= cg.read_only; // false if ANY group allows write
existing.hide_passwords &= cg.hide_passwords; // false if ANY group allows password view
existing.manage |= cg.manage; // true if ANY group allows manage
})
.or_insert(cg);
combined_permissions
},
)
} else {
HashMap::new()
};

2
src/api/core/mod.rs

@ -226,7 +226,7 @@ fn config() -> Json<Value> {
"url": "https://github.com/dani-garcia/vaultwarden"
},
"settings": {
"disableUserRegistration": crate::CONFIG.is_signup_disabled(),
"disableUserRegistration": crate::CONFIG.is_signup_disabled()
},
"environment": {
"vault": domain,

9
src/api/core/organizations.rs

@ -758,15 +758,6 @@ async fn delete_organization_collection(
_delete_organization_collection(&org_id, &col_id, &headers, &mut conn).await
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct DeleteCollectionData {
#[allow(dead_code)]
id: String,
#[allow(dead_code)]
org_id: OrganizationId,
}
#[post("/organizations/<org_id>/collections/<col_id>/delete")]
async fn post_organization_collection_delete(
org_id: OrganizationId,

2
src/api/web.rs

@ -60,7 +60,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
"mail_2fa_enabled": CONFIG._enable_email_2fa(),
"mail_enabled": CONFIG.mail_enabled(),
"sends_allowed": CONFIG.sends_allowed(),
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
"signup_disabled": CONFIG.is_signup_disabled(),
"sso_disabled": !CONFIG.sso_enabled(),
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(),
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),

25
src/config.rs

@ -1431,14 +1431,6 @@ impl Config {
self.update_config(builder, false).await
}
// The `signups_allowed` setting is overrided if:
// - The email whitelist is not empty (will allow signups).
// - The sso is activated and password login is disabled (will disable signups).
pub fn is_signup_disabled(&self) -> bool {
(!self.signups_allowed() && self.signups_domains_whitelist().is_empty())
|| (self.sso_enabled() && self.sso_only())
}
/// Tests whether an email's domain is allowed. A domain is allowed if it
/// is in signups_domains_whitelist, or if no whitelist is set (so there
/// are no domain restrictions in effect).
@ -1457,7 +1449,22 @@ impl Config {
/// Tests whether signup is allowed for an email address, taking into
/// account the signups_allowed and signups_domains_whitelist settings.
pub fn is_signup_allowed(&self, email: &str) -> bool {
!self.is_signup_disabled() && self.is_email_domain_allowed(email)
if !self.signups_domains_whitelist().is_empty() {
// The whitelist setting overrides the signups_allowed setting.
self.is_email_domain_allowed(email)
} else {
self.signups_allowed()
}
}
// The registration link should be hidden if
// - Signup is not allowed and email whitelist is empty unless mail is disabled and invitations are allowed
// - The sso is activated and password login is disabled.
pub fn is_signup_disabled(&self) -> bool {
(!self.signups_allowed()
&& self.signups_domains_whitelist().is_empty()
&& (self.mail_enabled() || !self.invitations_allowed()))
|| (self.sso_enabled() && self.sso_only())
}
/// Tests whether the specified user is allowed to create an organization.

24
src/db/models/cipher.rs

@ -609,22 +609,23 @@ impl Cipher {
let mut rows: Vec<(bool, bool, bool)> = Vec::new();
if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {
for collection in collections {
//User permissions
// User permissions
if let Some(cu) = cipher_sync_data.user_collections.get(collection) {
rows.push((cu.read_only, cu.hide_passwords, cu.manage));
}
//Group permissions
if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {
// Group permissions
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {
rows.push((cg.read_only, cg.hide_passwords, cg.manage));
}
}
}
rows
} else {
let mut access_flags = self.get_user_collections_access_flags(user_uuid, conn).await;
access_flags.append(&mut self.get_group_collections_access_flags(user_uuid, conn).await);
access_flags
let user_permissions = self.get_user_collections_access_flags(user_uuid, conn).await;
if !user_permissions.is_empty() {
user_permissions
} else {
self.get_group_collections_access_flags(user_uuid, conn).await
}
};
if rows.is_empty() {
@ -633,6 +634,9 @@ impl Cipher {
}
// A cipher can be in multiple collections with inconsistent access flags.
// Also, user permission overrule group permissions
// and only user permissions are returned by the code above.
//
// For example, a cipher could be in one collection where the user has
// read-only access, but also in another collection where the user has
// read/write access. For a flag to be in effect for a cipher, upstream
@ -641,13 +645,15 @@ impl Cipher {
// and `hide_passwords` columns. This could ideally be done as part of the
// query, but Diesel doesn't support a min() or bool_and() function on
// booleans and this behavior isn't portable anyway.
//
// The only exception is for the `manage` flag, that needs a boolean OR!
let mut read_only = true;
let mut hide_passwords = true;
let mut manage = false;
for (ro, hp, mn) in rows.iter() {
read_only &= ro;
hide_passwords &= hp;
manage &= mn;
manage |= mn;
}
Some((read_only, hide_passwords, manage))

8
src/db/models/collection.rs

@ -97,13 +97,13 @@ impl Collection {
(
cu.read_only,
cu.hide_passwords,
cu.manage || (is_manager && !cu.read_only && !cu.hide_passwords),
is_manager && (cu.manage || (!cu.read_only && !cu.hide_passwords)),
)
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {
(
cg.read_only,
cg.hide_passwords,
cg.manage || (is_manager && !cg.read_only && !cg.hide_passwords),
is_manager && (cg.manage || (!cg.read_only && !cg.hide_passwords)),
)
} else {
(false, false, false)
@ -114,7 +114,9 @@ impl Collection {
} else {
match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
Some(_) if self.is_manageable_by_user(user_uuid, conn).await => (false, false, true),
Some(m) if m.atype == MembershipType::Manager && self.is_manageable_by_user(user_uuid, conn).await => {
(false, false, true)
}
Some(m) => {
let is_manager = m.atype == MembershipType::Manager;
let read_only = !self.is_writable_by_user(user_uuid, conn).await;

Loading…
Cancel
Save