Browse Source

Merge branch 'main' into register_verify_email

register_verify_email
Daniel García 1 week ago
committed by GitHub
parent
commit
23b0863091
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      .env.template
  2. 7
      .github/workflows/build.yml
  3. 2
      .github/workflows/hadolint.yml
  4. 12
      .github/workflows/release.yml
  5. 242
      Cargo.lock
  6. 10
      Cargo.toml
  7. 8
      docker/DockerSettings.yaml
  8. 12
      docker/Dockerfile.alpine
  9. 14
      docker/Dockerfile.debian
  10. 7
      src/api/core/accounts.rs
  11. 14
      src/api/core/mod.rs
  12. 264
      src/api/core/organizations.rs
  13. 37
      src/api/core/public.rs
  14. 46
      src/api/core/two_factor/authenticator.rs
  15. 39
      src/api/web.rs
  16. 44
      src/config.rs
  17. 34
      src/db/models/cipher.rs
  18. 5
      src/db/models/collection.rs
  19. 5
      src/db/models/group.rs
  20. 116
      src/db/models/organization.rs
  21. 2
      src/static/scripts/admin_users.js
  22. 55
      src/static/templates/scss/vaultwarden.scss.hbs

1
.env.template

@ -351,6 +351,7 @@
## - "browser-fileless-import": Directly import credentials from other providers without a file.
## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension)
## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor.
## - "inline-menu-positioning-improvements": Enable the use of inline menu password generator and identity suggestions in the browser extension.
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials

7
.github/workflows/build.yml

@ -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@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa # master @ Nov 18, 2024, 5:36 AM GMT+1
uses: dtolnay/rust-toolchain@a54c7afa936fefeb4456b2dd8068152669aa8203 # master @ Dec 14, 2024, 5:49 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@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa # master @ Nov 18, 2024, 5:36 AM GMT+1
uses: dtolnay/rust-toolchain@a54c7afa936fefeb4456b2dd8068152669aa8203 # master @ Dec 14, 2024, 5:49 AM GMT+1
if: ${{ matrix.channel != 'rust-toolchain' }}
with:
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
@ -107,7 +107,8 @@ jobs:
# End Show environment
# Enable Rust Caching
- uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
- name: Rust Caching
uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
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.

2
.github/workflows/hadolint.yml

@ -18,7 +18,7 @@ jobs:
# Start Docker Buildx
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
# https://github.com/moby/buildkit/issues/3969
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
with:

12
.github/workflows/release.yml

@ -69,7 +69,7 @@ jobs:
# Start Docker Buildx
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
# https://github.com/moby/buildkit/issues/3969
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
with:
@ -165,7 +165,7 @@ jobs:
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}"
- name: Bake ${{ matrix.base_image }} containers
uses: docker/bake-action@2e3d19baedb14545e5d41222653874f25d5b4dfb # v5.10.0
uses: docker/bake-action@3fc70e1131fee40a422dd8dd0ff22014ae20a1f3 # v5.11.0
env:
BASE_TAGS: "${{ env.BASE_TAGS }}"
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
@ -223,28 +223,28 @@ jobs:
# Upload artifacts to Github Actions
- name: "Upload amd64 artifact"
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
if: ${{ matrix.base_image == 'alpine' }}
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64
path: vaultwarden-amd64
- name: "Upload arm64 artifact"
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
if: ${{ matrix.base_image == 'alpine' }}
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64
path: vaultwarden-arm64
- name: "Upload armv7 artifact"
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
if: ${{ matrix.base_image == 'alpine' }}
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7
path: vaultwarden-armv7
- name: "Upload armv6 artifact"
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
if: ${{ matrix.base_image == 'alpine' }}
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6

242
Cargo.lock

@ -176,7 +176,7 @@ version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
dependencies = [
"event-listener 5.3.1",
"event-listener 5.4.0",
"event-listener-strategy",
"pin-project-lite",
]
@ -194,7 +194,7 @@ dependencies = [
"async-task",
"blocking",
"cfg-if",
"event-listener 5.3.1",
"event-listener 5.4.0",
"futures-lite",
"rustix",
"tracing",
@ -275,9 +275,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.83"
version = "0.1.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
dependencies = [
"proc-macro2",
"quote",
@ -435,9 +435,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.20.0"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "byteorder"
@ -489,9 +489,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]]
name = "cc"
version = "1.2.4"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
dependencies = [
"shlex",
]
@ -637,9 +637,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
@ -721,6 +721,37 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "devise"
version = "0.4.2"
@ -918,6 +949,12 @@ dependencies = [
"syn",
]
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "equivalent"
version = "1.0.1"
@ -942,9 +979,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "5.3.1"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
dependencies = [
"concurrent-queue",
"parking",
@ -957,7 +994,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
dependencies = [
"event-listener 5.3.1",
"event-listener 5.4.0",
"pin-project-lite",
]
@ -1185,9 +1222,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "gloo-timers"
@ -1262,17 +1299,18 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]]
name = "handlebars"
version = "6.2.0"
version = "6.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd4ccde012831f9a071a637b0d4e31df31c0f6c525784b35ae76a9ac6bc1e315"
checksum = "3d6b224b95c1e668ac0270325ad563b2eef1469fbbb8959bc7c692c844b813d9"
dependencies = [
"derive_builder",
"log",
"num-order",
"pest",
"pest_derive",
"serde",
"serde_json",
"thiserror 1.0.69",
"thiserror 2.0.9",
"walkdir",
]
@ -1364,15 +1402,6 @@ dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "hostname"
version = "0.3.1"
@ -1474,9 +1503,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "0.14.31"
version = "0.14.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
dependencies = [
"bytes",
"futures-channel",
@ -1497,9 +1526,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.5.1"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
dependencies = [
"bytes",
"futures-channel",
@ -1517,13 +1546,13 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.27.3"
version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http 1.2.0",
"hyper 1.5.1",
"hyper 1.5.2",
"hyper-util",
"rustls 0.23.20",
"rustls-pki-types",
@ -1540,7 +1569,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.5.1",
"hyper 1.5.2",
"hyper-util",
"native-tls",
"tokio",
@ -1559,7 +1588,7 @@ dependencies = [
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"hyper 1.5.1",
"hyper 1.5.2",
"pin-project-lite",
"socket2",
"tokio",
@ -1886,9 +1915,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.168"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libm"
@ -1925,9 +1954,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "litemap"
@ -2049,9 +2078,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
dependencies = [
"adler2",
]
@ -2226,9 +2255,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.36.5"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
@ -2400,7 +2429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
dependencies = [
"memchr",
"thiserror 2.0.7",
"thiserror 2.0.9",
"ucd-trie",
]
@ -2440,9 +2469,9 @@ dependencies = [
[[package]]
name = "phf"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros",
"phf_shared",
@ -2450,9 +2479,9 @@ dependencies = [
[[package]]
name = "phf_codegen"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator",
"phf_shared",
@ -2460,9 +2489,9 @@ dependencies = [
[[package]]
name = "phf_generator"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand",
@ -2470,9 +2499,9 @@ dependencies = [
[[package]]
name = "phf_macros"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator",
"phf_shared",
@ -2483,9 +2512,9 @@ dependencies = [
[[package]]
name = "phf_shared"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
@ -2498,9 +2527,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project-lite"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
@ -2619,9 +2648,9 @@ dependencies = [
[[package]]
name = "quanta"
version = "0.12.4"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773ce68d0bb9bc7ef20be3536ffe94e223e1f365bd374108b2659fac0c65cfe6"
checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e"
dependencies = [
"crossbeam-utils",
"libc",
@ -2640,9 +2669,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.37"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
@ -2789,9 +2818,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.12.9"
version = "0.12.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
dependencies = [
"async-compression",
"base64 0.22.1",
@ -2806,7 +2835,7 @@ dependencies = [
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.1",
"hyper 1.5.2",
"hyper-rustls",
"hyper-tls",
"hyper-util",
@ -2828,6 +2857,7 @@ dependencies = [
"tokio-native-tls",
"tokio-socks",
"tokio-util",
"tower",
"tower-service",
"url",
"wasm-bindgen",
@ -2948,7 +2978,7 @@ dependencies = [
"either",
"futures",
"http 0.2.12",
"hyper 0.14.31",
"hyper 0.14.32",
"indexmap",
"log",
"memchr",
@ -3007,9 +3037,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "0.38.42"
version = "0.38.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
dependencies = [
"bitflags",
"errno",
@ -3090,9 +3120,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
@ -3164,9 +3194,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.12.1"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
@ -3180,9 +3210,9 @@ checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
[[package]]
name = "serde"
version = "1.0.216"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
@ -3199,9 +3229,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.216"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@ -3210,9 +3240,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
dependencies = [
"itoa",
"memchr",
@ -3311,9 +3341,9 @@ dependencies = [
[[package]]
name = "siphasher"
version = "0.3.11"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
@ -3406,9 +3436,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.90"
version = "2.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
dependencies = [
"proc-macro2",
"quote",
@ -3470,12 +3500,13 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.14.0"
version = "3.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
dependencies = [
"cfg-if",
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys 0.59.0",
@ -3492,11 +3523,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.7"
version = "2.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767"
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
dependencies = [
"thiserror-impl 2.0.7",
"thiserror-impl 2.0.9",
]
[[package]]
@ -3512,9 +3543,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.7"
version = "2.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36"
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
dependencies = [
"proc-macro2",
"quote",
@ -3585,9 +3616,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.8.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
dependencies = [
"tinyvec_macros",
]
@ -3751,6 +3782,27 @@ dependencies = [
"sha2",
]
[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
@ -4165,12 +4217,12 @@ dependencies = [
[[package]]
name = "which"
version = "7.0.0"
version = "7.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b"
checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028"
dependencies = [
"either",
"home",
"env_home",
"rustix",
"winsafe",
]
@ -4420,9 +4472,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
version = "0.6.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
dependencies = [
"memchr",
]

10
Cargo.toml

@ -70,8 +70,8 @@ futures = "0.3.31"
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.216", features = ["derive"] }
serde_json = "1.0.133"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.135"
# A safe, extensible ORM and Query builder
diesel = { version = "2.2.6", features = ["chrono", "r2d2", "numeric"] }
@ -120,10 +120,10 @@ percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
email_address = "0.2.9"
# HTML Template library
handlebars = { version = "6.2.0", features = ["dir_source"] }
handlebars = { version = "6.3.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"] }
reqwest = { version = "0.12.12", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
hickory-resolver = "0.24.2"
# Favicon extraction libraries
@ -155,7 +155,7 @@ semver = "1.0.24"
# Allow overriding the default memory allocator
# Mainly used for the musl builds, since the default musl malloc is very slow
mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true }
which = "7.0.0"
which = "7.0.1"
# Argon2 library with support for the PHC format
argon2 = "0.5.3"

8
docker/DockerSettings.yaml

@ -1,10 +1,10 @@
---
vault_version: "v2024.6.2c"
vault_image_digest: "sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b"
# Cross Compile Docker Helper Scripts v1.5.0
vault_version: "v2025.1.0"
vault_image_digest: "sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8"
# Cross Compile Docker Helper Scripts v1.6.1
# 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"
xx_image_digest: "sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894"
rust_version: 1.83.0 # Rust version to be used
debian_version: bookworm # Debian release name to be used
alpine_version: "3.21" # Alpine version to be used

12
docker/Dockerfile.alpine

@ -19,15 +19,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2c
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c
# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b]
# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.0
# [docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b
# [docker.io/vaultwarden/web-vault:v2024.6.2c]
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8
# [docker.io/vaultwarden/web-vault:v2025.1.0]
#
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8 AS vault
########################## ALPINE BUILD IMAGES ##########################
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64

14
docker/Dockerfile.debian

@ -19,20 +19,20 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2c
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c
# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b]
# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.0
# [docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b
# [docker.io/vaultwarden/web-vault:v2024.6.2c]
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8
# [docker.io/vaultwarden/web-vault:v2025.1.0]
#
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8 AS vault
########################## Cross Compile Docker Helper Scripts ##########################
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
## And these bash scripts do not have any significant difference if at all
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa AS xx
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894 AS xx
########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006

7
src/api/core/accounts.rs

@ -399,7 +399,12 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, mut conn: D
&data.new_master_password_hash,
Some(data.key),
true,
Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]),
Some(vec![
String::from("post_rotatekey"),
String::from("get_contacts"),
String::from("get_public_keys"),
String::from("get_api_webauthn"),
]),
);
let save_result = user.save(&mut conn).await;

14
src/api/core/mod.rs

@ -18,7 +18,7 @@ pub use sends::purge_sends;
pub fn routes() -> Vec<Route> {
let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
let mut hibp_routes = routes![hibp_breach];
let mut meta_routes = routes![alive, now, version, config];
let mut meta_routes = routes![alive, now, version, config, get_api_webauthn];
let mut routes = Vec::new();
routes.append(&mut accounts::routes());
@ -184,6 +184,18 @@ fn version() -> Json<&'static str> {
Json(crate::VERSION.unwrap_or_default())
}
#[get("/webauthn")]
fn get_api_webauthn(_headers: Headers) -> Json<Value> {
// Prevent a 404 error, which also causes key-rotation issues
// It looks like this is used when login with passkeys is enabled, which Vaultwarden does not (yet) support
// An empty list/data also works fine
Json(json!({
"object": "list",
"data": [],
"continuationToken": null
}))
}
#[get("/config")]
fn config() -> Json<Value> {
let domain = crate::CONFIG.domain();

264
src/api/core/organizations.rs

@ -48,6 +48,7 @@ pub fn routes() -> Vec<Route> {
confirm_invite,
bulk_confirm_invite,
accept_invite,
get_org_user_mini_details,
get_user,
edit_user,
put_organization_user,
@ -77,6 +78,7 @@ pub fn routes() -> Vec<Route> {
restore_organization_user,
bulk_restore_organization_user,
get_groups,
get_groups_details,
post_groups,
get_group,
put_group,
@ -98,6 +100,7 @@ pub fn routes() -> Vec<Route> {
get_org_export,
api_key,
rotate_api_key,
get_billing_metadata,
]
}
@ -322,7 +325,14 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose,
};
// get all collection memberships for the current organization
let coll_users = CollectionUser::find_by_organization(org_id, &mut conn).await;
let coll_users = CollectionUser::find_by_organization_swap_user_uuid_with_org_user_uuid(org_id, &mut conn).await;
// Generate a HashMap to get the correct UserOrgType per user to determine the manage permission
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
let users_org_type: HashMap<String, i32> = UserOrganization::find_confirmed_by_org(org_id, &mut conn)
.await
.into_iter()
.map(|uo| (uo.uuid, uo.atype))
.collect();
// check if current user has full access to the organization (either directly or via any group)
let has_full_access_to_org = user_org.access_all
@ -336,11 +346,22 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose,
|| (CONFIG.org_groups_enabled()
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &user_org.uuid, &mut conn).await);
// Not assigned collections should not be returned
if !assigned {
continue;
}
// get the users assigned directly to the given collection
let users: Vec<Value> = coll_users
.iter()
.filter(|collection_user| collection_user.collection_uuid == col.uuid)
.map(|collection_user| SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json())
.map(|collection_user| {
SelectionReadOnly::to_collection_user_details_read_only(
collection_user,
*users_org_type.get(&collection_user.user_uuid).unwrap_or(&(UserOrgType::User as i32)),
)
.to_json()
})
.collect();
// get the group details for the given collection
@ -645,12 +666,24 @@ async fn get_org_collection_detail(
Vec::with_capacity(0)
};
// Generate a HashMap to get the correct UserOrgType per user to determine the manage permission
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
let users_org_type: HashMap<String, i32> = UserOrganization::find_confirmed_by_org(org_id, &mut conn)
.await
.into_iter()
.map(|uo| (uo.uuid, uo.atype))
.collect();
let users: Vec<Value> =
CollectionUser::find_by_collection_swap_user_uuid_with_org_user_uuid(&collection.uuid, &mut conn)
.await
.iter()
.map(|collection_user| {
SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()
SelectionReadOnly::to_collection_user_details_read_only(
collection_user,
*users_org_type.get(&collection_user.user_uuid).unwrap_or(&(UserOrgType::User as i32)),
)
.to_json()
})
.collect();
@ -830,13 +863,19 @@ struct InviteData {
collections: Option<Vec<CollectionData>>,
#[serde(default)]
access_all: bool,
#[serde(default)]
permissions: HashMap<String, Value>,
}
#[post("/organizations/<org_id>/users/invite", data = "<data>")]
async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult {
let data: InviteData = data.into_inner();
let mut data: InviteData = data.into_inner();
let new_type = match UserOrgType::from_str(&data.r#type.into_string()) {
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
// The from_str() will convert the custom role type into a manager role type
let raw_type = &data.r#type.into_string();
// UserOrgType::from_str will convert custom (4) to manager (3)
let new_type = match UserOrgType::from_str(raw_type) {
Some(new_type) => new_type as i32,
None => err!("Invalid type"),
};
@ -845,6 +884,18 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
err!("Only Owners can invite Managers, Admins or Owners")
}
// HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag
// Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes
// If the box is not checked, the user will still be a manager, but not with the access_all permission
if raw_type.eq("4")
&& data.permissions.get("editAnyCollection") == Some(&json!(true))
&& data.permissions.get("deleteAnyCollection") == Some(&json!(true))
&& data.permissions.get("createNewCollections") == Some(&json!(true))
{
data.access_all = true;
}
let mut user_created: bool = false;
for email in data.emails.iter() {
let mut user_org_status = UserOrgStatus::Invited as i32;
let user = match User::find_by_mail(email, &mut conn).await {
@ -858,13 +909,13 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
}
if !CONFIG.mail_enabled() {
let invitation = Invitation::new(email);
invitation.save(&mut conn).await?;
Invitation::new(email).save(&mut conn).await?;
}
let mut user = User::new(email.clone());
user.save(&mut conn).await?;
user
let mut new_user = User::new(email.clone());
new_user.save(&mut conn).await?;
user_created = true;
new_user
}
Some(user) => {
if UserOrganization::find_by_user_and_org(&user.uuid, org_id, &mut conn).await.is_some() {
@ -879,11 +930,49 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
}
};
let mut new_user = UserOrganization::new(user.uuid.clone(), String::from(org_id));
let mut new_member = UserOrganization::new(user.uuid.clone(), String::from(org_id));
let access_all = data.access_all;
new_user.access_all = access_all;
new_user.atype = new_type;
new_user.status = user_org_status;
new_member.access_all = access_all;
new_member.atype = new_type;
new_member.status = user_org_status;
new_member.save(&mut conn).await?;
if CONFIG.mail_enabled() {
let org_name = match Organization::find_by_uuid(org_id, &mut conn).await {
Some(org) => org.name,
None => err!("Error looking up organization"),
};
if let Err(e) = mail::send_invite(
&user,
Some(String::from(org_id)),
Some(new_member.uuid.clone()),
&org_name,
Some(headers.user.email.clone()),
)
.await
{
// Upon error delete the user, invite and org member records when needed
if user_created {
user.delete(&mut conn).await?;
} else {
new_member.delete(&mut conn).await?;
}
err!(format!("Error sending invite: {e:?} "));
};
}
log_event(
EventType::OrganizationUserInvited as i32,
&new_member.uuid.clone(),
org_id,
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
)
.await;
// If no accessAll, add the collections received
if !access_all {
@ -904,39 +993,10 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
}
}
new_user.save(&mut conn).await?;
for group in data.groups.iter() {
let mut group_entry = GroupUser::new(String::from(group), user.uuid.clone());
let mut group_entry = GroupUser::new(String::from(group), new_member.uuid.clone());
group_entry.save(&mut conn).await?;
}
log_event(
EventType::OrganizationUserInvited as i32,
&new_user.uuid,
org_id,
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
)
.await;
if CONFIG.mail_enabled() {
let org_name = match Organization::find_by_uuid(org_id, &mut conn).await {
Some(org) => org.name,
None => err!("Error looking up organization"),
};
mail::send_invite(
&user,
Some(String::from(org_id)),
Some(new_user.uuid),
&org_name,
Some(headers.user.email.clone()),
)
.await?;
}
}
Ok(())
@ -1014,7 +1074,7 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co
let invitation = Invitation::new(&user.email);
invitation.save(conn).await?;
} else {
let _ = Invitation::take(&user.email, conn).await;
Invitation::take(&user.email, conn).await;
let mut user_org = user_org;
user_org.status = UserOrgStatus::Accepted as i32;
user_org.save(conn).await?;
@ -1254,7 +1314,21 @@ async fn _confirm_invite(
save_result
}
#[get("/organizations/<org_id>/users/<org_user_id>?<data..>")]
#[get("/organizations/<org_id>/users/mini-details", rank = 1)]
async fn get_org_user_mini_details(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
let mut users_json = Vec::new();
for u in UserOrganization::find_by_org(org_id, &mut conn).await {
users_json.push(u.to_json_mini_details(&mut conn).await);
}
Json(json!({
"data": users_json,
"object": "list",
"continuationToken": null,
}))
}
#[get("/organizations/<org_id>/users/<org_user_id>?<data..>", rank = 2)]
async fn get_user(
org_id: &str,
org_user_id: &str,
@ -1282,6 +1356,8 @@ struct EditUserData {
groups: Option<Vec<String>>,
#[serde(default)]
access_all: bool,
#[serde(default)]
permissions: HashMap<String, Value>,
}
#[put("/organizations/<org_id>/users/<org_user_id>", data = "<data>", rank = 1)]
@ -1303,14 +1379,30 @@ async fn edit_user(
headers: AdminHeaders,
mut conn: DbConn,
) -> EmptyResult {
let data: EditUserData = data.into_inner();
let mut data: EditUserData = data.into_inner();
let Some(new_type) = UserOrgType::from_str(&data.r#type.into_string()) else {
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
// The from_str() will convert the custom role type into a manager role type
let raw_type = &data.r#type.into_string();
// UserOrgType::from_str will convert custom (4) to manager (3)
let Some(new_type) = UserOrgType::from_str(raw_type) else {
err!("Invalid type")
};
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")
// HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag
// Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes
// If the box is not checked, the user will still be a manager, but not with the access_all permission
if raw_type.eq("4")
&& data.permissions.get("editAnyCollection") == Some(&json!(true))
&& data.permissions.get("deleteAnyCollection") == Some(&json!(true))
&& data.permissions.get("createNewCollections") == Some(&json!(true))
{
data.access_all = true;
}
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"),
};
if new_type != user_to_edit.atype
@ -1901,6 +1993,12 @@ fn get_plans_tax_rates(_headers: Headers) -> Json<Value> {
Json(_empty_data_json())
}
#[get("/organizations/<_org_id>/billing/metadata")]
fn get_billing_metadata(_org_id: &str, _headers: Headers) -> Json<Value> {
// Prevent a 404 error, which also causes Javascript errors.
Json(_empty_data_json())
}
fn _empty_data_json() -> Value {
json!({
"object": "list",
@ -1938,6 +2036,9 @@ struct OrgImportData {
users: Vec<OrgImportUserData>,
}
/// This function seems to be deprected
/// It is only used with older directory connectors
/// TODO: Cleanup Tech debt
#[post("/organizations/<org_id>/import", data = "<data>")]
async fn import(org_id: &str, data: Json<OrgImportData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
let data = data.into_inner();
@ -1981,23 +2082,10 @@ async fn import(org_id: &str, data: Json<OrgImportData>, headers: Headers, mut c
UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
};
let mut new_org_user = UserOrganization::new(user.uuid.clone(), String::from(org_id));
new_org_user.access_all = false;
new_org_user.atype = UserOrgType::User as i32;
new_org_user.status = user_org_status;
new_org_user.save(&mut conn).await?;
log_event(
EventType::OrganizationUserInvited as i32,
&new_org_user.uuid,
org_id,
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
)
.await;
let mut new_member = UserOrganization::new(user.uuid.clone(), String::from(org_id));
new_member.access_all = false;
new_member.atype = UserOrgType::User as i32;
new_member.status = user_org_status;
if CONFIG.mail_enabled() {
let org_name = match Organization::find_by_uuid(org_id, &mut conn).await {
@ -2008,12 +2096,27 @@ async fn import(org_id: &str, data: Json<OrgImportData>, headers: Headers, mut c
mail::send_invite(
&user,
Some(String::from(org_id)),
Some(new_org_user.uuid),
Some(new_member.uuid.clone()),
&org_name,
Some(headers.user.email.clone()),
)
.await?;
}
// Save the member after sending an email
// If sending fails the member will not be saved to the database, and will not result in the admin needing to reinvite the users manually
new_member.save(&mut conn).await?;
log_event(
EventType::OrganizationUserInvited as i32,
&new_member.uuid,
org_id,
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
)
.await;
}
}
}
@ -2299,6 +2402,11 @@ async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbCon
})))
}
#[get("/organizations/<org_id>/groups/details", rank = 1)]
async fn get_groups_details(org_id: &str, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
get_groups(org_id, headers, conn).await
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct GroupRequest {
@ -2331,6 +2439,7 @@ struct SelectionReadOnly {
id: String,
read_only: bool,
hide_passwords: bool,
manage: bool,
}
impl SelectionReadOnly {
@ -2339,18 +2448,31 @@ impl SelectionReadOnly {
}
pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly {
// If both read_only and hide_passwords are false, then manage should be true
// You can't have an entry with read_only and manage, or hide_passwords and manage
// Or an entry with everything to false
SelectionReadOnly {
id: collection_group.groups_uuid.clone(),
read_only: collection_group.read_only,
hide_passwords: collection_group.hide_passwords,
manage: !collection_group.read_only && !collection_group.hide_passwords,
}
}
pub fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> SelectionReadOnly {
pub fn to_collection_user_details_read_only(
collection_user: &CollectionUser,
user_org_type: i32,
) -> SelectionReadOnly {
// Vaultwarden allows manage access for Admins and Owners by default
// For managers (Or custom role) it depends if they have read_ony or hide_passwords set to true or not
SelectionReadOnly {
id: collection_user.user_uuid.clone(),
read_only: collection_user.read_only,
hide_passwords: collection_user.hide_passwords,
manage: user_org_type >= UserOrgType::Admin
|| (user_org_type == UserOrgType::Manager
&& !collection_user.read_only
&& !collection_user.hide_passwords),
}
}
@ -2534,7 +2656,7 @@ async fn bulk_delete_groups(
Ok(())
}
#[get("/organizations/<org_id>/groups/<group_id>")]
#[get("/organizations/<org_id>/groups/<group_id>", rank = 2)]
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");
@ -2904,7 +3026,7 @@ async fn put_reset_password_enrollment(
if reset_request.reset_password_key.is_none()
&& OrgPolicy::org_is_reset_password_auto_enroll(org_id, &mut conn).await
{
err!("Reset password can't be withdrawed due to an enterprise policy");
err!("Reset password can't be withdrawn due to an enterprise policy");
}
if reset_request.reset_password_key.is_some() {

37
src/api/core/public.rs

@ -52,6 +52,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
let data = data.into_inner();
for user_data in &data.members {
let mut user_created: bool = false;
if user_data.deleted {
// If user is marked for deletion and it exists, revoke it
if let Some(mut user_org) =
@ -97,9 +98,9 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
new_user.save(&mut conn).await?;
if !CONFIG.mail_enabled() {
let invitation = Invitation::new(&new_user.email);
invitation.save(&mut conn).await?;
Invitation::new(&new_user.email).save(&mut conn).await?;
}
user_created = true;
new_user
}
};
@ -109,13 +110,13 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
};
let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
new_org_user.set_external_id(Some(user_data.external_id.clone()));
new_org_user.access_all = false;
new_org_user.atype = UserOrgType::User as i32;
new_org_user.status = user_org_status;
let mut new_member = UserOrganization::new(user.uuid.clone(), org_id.clone());
new_member.set_external_id(Some(user_data.external_id.clone()));
new_member.access_all = false;
new_member.atype = UserOrgType::User as i32;
new_member.status = user_org_status;
new_org_user.save(&mut conn).await?;
new_member.save(&mut conn).await?;
if CONFIG.mail_enabled() {
let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await {
@ -123,8 +124,24 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
None => err!("Error looking up organization"),
};
mail::send_invite(&user, Some(org_id.clone()), Some(new_org_user.uuid), &org_name, Some(org_email))
.await?;
if let Err(e) = mail::send_invite(
&user,
Some(org_id.clone()),
Some(new_member.uuid.clone()),
&org_name,
Some(org_email),
)
.await
{
// Upon error delete the user, invite and org member records when needed
if user_created {
user.delete(&mut conn).await?;
} else {
new_member.delete(&mut conn).await?;
}
err!(format!("Error sending invite: {e:?} "));
}
}
}
}

46
src/api/core/two_factor/authenticator.rs

@ -16,7 +16,7 @@ use crate::{
pub use crate::config::CONFIG;
pub fn routes() -> Vec<Route> {
routes![generate_authenticator, activate_authenticator, activate_authenticator_put,]
routes![generate_authenticator, activate_authenticator, activate_authenticator_put, disable_authenticator]
}
#[post("/two-factor/get-authenticator", data = "<data>")]
@ -175,3 +175,47 @@ pub async fn validate_totp_code(
}
);
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DisableAuthenticatorData {
key: String,
master_password_hash: String,
r#type: NumberOrString,
}
#[delete("/two-factor/authenticator", data = "<data>")]
async fn disable_authenticator(data: Json<DisableAuthenticatorData>, headers: Headers, mut conn: DbConn) -> JsonResult {
let user = headers.user;
let type_ = data.r#type.into_i32()?;
if !user.check_valid_password(&data.master_password_hash) {
err!("Invalid password");
}
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
if twofactor.data == data.key {
twofactor.delete(&mut conn).await?;
log_user_event(
EventType::UserDisabled2fa as i32,
&user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
)
.await;
} else {
err!(format!("TOTP key for user {} does not match recorded value, cannot deactivate", &user.email));
}
}
if TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty() {
super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await?;
}
Ok(Json(json!({
"enabled": false,
"keys": type_,
"object": "twoFactorProvider"
})))
}

39
src/api/web.rs

@ -1,4 +1,3 @@
use once_cell::sync::Lazy;
use std::path::{Path, PathBuf};
use rocket::{
@ -14,7 +13,7 @@ use crate::{
api::{core::now, ApiResult, EmptyResult},
auth::decode_file_download,
error::Error,
util::{get_web_vault_version, Cached, SafeString},
util::{Cached, SafeString},
CONFIG,
};
@ -54,43 +53,7 @@ fn not_found() -> ApiResult<Html<String>> {
#[get("/css/vaultwarden.css")]
fn vaultwarden_css() -> Cached<Css<String>> {
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static WEB_VAULT_VERSION: Lazy<u32> = Lazy::new(|| {
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
let vault_version = get_web_vault_version();
let (major, minor, patch) = match re.captures(&vault_version) {
Some(c) if c.len() == 4 => (
c.get(1).unwrap().as_str().parse().unwrap(),
c.get(2).unwrap().as_str().parse().unwrap(),
c.get(3).unwrap().as_str().parse().unwrap(),
),
_ => (2024, 6, 2),
};
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
});
// Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static VW_VERSION: Lazy<u32> = Lazy::new(|| {
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();
let vw_version = crate::VERSION.unwrap_or("1.32.1");
let (major, minor, patch) = match re.captures(vw_version) {
Some(c) if c.len() == 4 => (
c.get(1).unwrap().as_str().parse().unwrap(),
c.get(2).unwrap().as_str().parse().unwrap(),
c.get(3).unwrap().as_str().parse().unwrap(),
),
_ => (1, 32, 1),
};
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
});
let css_options = json!({
"web_vault_version": *WEB_VAULT_VERSION,
"vw_version": *VW_VERSION,
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
"mail_enabled": CONFIG.mail_enabled(),
"yubico_enabled": CONFIG._enable_yubico() && (CONFIG.yubico_client_id().is_some() == CONFIG.yubico_secret_key().is_some()),

44
src/config.rs

@ -12,7 +12,7 @@ use reqwest::Url;
use crate::{
db::DbConnType,
error::Error,
util::{get_env, get_env_bool, parse_experimental_client_feature_flags},
util::{get_env, get_env_bool, get_web_vault_version, parse_experimental_client_feature_flags},
};
static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
@ -830,6 +830,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
"browser-fileless-import",
"extension-refresh",
"fido2-vault-credentials",
"inline-menu-positioning-improvements",
"ssh-key-vault-item",
"ssh-agent",
];
@ -1327,6 +1328,8 @@ where
// Register helpers
hb.register_helper("case", Box::new(case_helper));
hb.register_helper("to_json", Box::new(to_json));
hb.register_helper("webver", Box::new(webver));
hb.register_helper("vwver", Box::new(vwver));
macro_rules! reg {
($name:expr) => {{
@ -1431,3 +1434,42 @@ fn to_json<'reg, 'rc>(
out.write(&json)?;
Ok(())
}
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static WEB_VAULT_VERSION: Lazy<semver::Version> = Lazy::new(|| {
let vault_version = get_web_vault_version();
// Use a single regex capture to extract version components
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
re.captures(&vault_version)
.and_then(|c| {
(c.len() == 4).then(|| {
format!("{}.{}.{}", c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str(), c.get(3).unwrap().as_str())
})
})
.and_then(|v| semver::Version::parse(&v).ok())
.unwrap_or_else(|| semver::Version::parse("2024.6.2").unwrap())
});
// Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static VW_VERSION: Lazy<semver::Version> = Lazy::new(|| {
let vw_version = crate::VERSION.unwrap_or("1.32.5");
// Use a single regex capture to extract version components
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();
re.captures(vw_version)
.and_then(|c| {
(c.len() == 4).then(|| {
format!("{}.{}.{}", c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str(), c.get(3).unwrap().as_str())
})
})
.and_then(|v| semver::Version::parse(&v).ok())
.unwrap_or_else(|| semver::Version::parse("1.32.5").unwrap())
});
handlebars::handlebars_helper!(webver: | web_vault_version: String |
semver::VersionReq::parse(&web_vault_version).expect("Invalid web-vault version compare string").matches(&WEB_VAULT_VERSION)
);
handlebars::handlebars_helper!(vwver: | vw_version: String |
semver::VersionReq::parse(&vw_version).expect("Invalid Vaultwarden version compare string").matches(&VW_VERSION)
);

34
src/db/models/cipher.rs

@ -241,12 +241,23 @@ impl Cipher {
// NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream
// Set the first element of the Uris array as Uri, this is needed several (mobile) clients.
if self.atype == 1 {
if type_data_json["uris"].is_array() {
let uri = type_data_json["uris"][0]["uri"].clone();
type_data_json["uri"] = uri;
} else {
// Upstream always has an Uri key/value
// Upstream always has an `uri` key/value
type_data_json["uri"] = Value::Null;
if let Some(uris) = type_data_json["uris"].as_array_mut() {
if !uris.is_empty() {
// Fix uri match values first, they are only allowed to be a number or null
// If it is a string, convert it to an int or null if that fails
for uri in &mut *uris {
if uri["match"].is_string() {
let match_value = match uri["match"].as_str().unwrap_or_default().parse::<u8>() {
Ok(n) => json!(n),
_ => Value::Null,
};
uri["match"] = match_value;
}
}
type_data_json["uri"] = uris[0]["uri"].clone();
}
}
}
@ -261,6 +272,19 @@ impl Cipher {
}
}
// Fix invalid SSH Entries
// This breaks at least the native mobile client if invalid
// The only way to fix this is by setting type_data_json to `null`
// Opening this ssh-key in the mobile client will probably crash the client, but you can edit, save and afterwards delete it
if self.atype == 5
&& (type_data_json["keyFingerprint"].as_str().is_none_or(|v| v.is_empty())
|| type_data_json["privateKey"].as_str().is_none_or(|v| v.is_empty())
|| type_data_json["publicKey"].as_str().is_none_or(|v| v.is_empty()))
{
warn!("Error parsing ssh-key, mandatory fields are invalid for {}", self.uuid);
type_data_json = Value::Null;
}
// Clone the type_data and add some default value.
let mut data_json = type_data_json.clone();

5
src/db/models/collection.rs

@ -511,7 +511,10 @@ impl CollectionUser {
}}
}
pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_organization_swap_user_uuid_with_org_user_uuid(
org_uuid: &str,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_collections::table
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))

5
src/db/models/group.rs

@ -74,6 +74,9 @@ impl Group {
}
pub async fn to_json_details(&self, conn: &mut DbConn) -> Value {
// If both read_only and hide_passwords are false, then manage should be true
// You can't have an entry with read_only and manage, or hide_passwords and manage
// Or an entry with everything to false
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
.await
.iter()
@ -82,7 +85,7 @@ impl Group {
"id": entry.collections_uuid,
"readOnly": entry.read_only,
"hidePasswords": entry.hide_passwords,
"manage": false
"manage": !entry.read_only && !entry.hide_passwords,
})
})
.collect();

116
src/db/models/organization.rs

@ -73,6 +73,8 @@ impl UserOrgType {
"1" | "Admin" => Some(UserOrgType::Admin),
"2" | "User" => Some(UserOrgType::User),
"3" | "Manager" => Some(UserOrgType::Manager),
// HACK: We convert the custom role to a manager role
"4" | "Custom" => Some(UserOrgType::Manager),
_ => None,
}
}
@ -85,7 +87,7 @@ impl Ord for UserOrgType {
3, // Owner
2, // Admin
0, // User
1, // Manager
1, // Manager && Custom
];
ACCESS_LEVEL[*self as usize].cmp(&ACCESS_LEVEL[*other as usize])
}
@ -158,33 +160,46 @@ impl Organization {
pub fn to_json(&self) -> Value {
json!({
"id": self.uuid,
"identifier": null, // not supported by us
"name": self.name,
"seats": null,
"maxCollections": null,
"maxStorageGb": i16::MAX, // The value doesn't matter, we don't check server-side
"use2fa": true,
"useCustomPermissions": false,
"useCustomPermissions": true,
"useDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
"useEvents": CONFIG.org_events_enabled(),
"useGroups": CONFIG.org_groups_enabled(),
"useTotp": true,
"usePolicies": true,
// "useScim": false, // Not supported (Not AGPLv3 Licensed)
"useScim": false, // Not supported (Not AGPLv3 Licensed)
"useSso": false, // Not supported
// "useKeyConnector": false, // Not supported
"useKeyConnector": false, // Not supported
"usePasswordManager": true,
"useSecretsManager": false, // Not supported (Not AGPLv3 Licensed)
"selfHost": true,
"useApi": true,
"hasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
"useResetPassword": CONFIG.mail_enabled(),
"allowAdminAccessToAllCollectionItems": true,
"limitCollectionCreation": true,
"limitCollectionCreationDeletion": true,
"limitCollectionDeletion": true,
"businessName": null,
"businessName": self.name,
"businessAddress1": null,
"businessAddress2": null,
"businessAddress3": null,
"businessCountry": null,
"businessTaxNumber": null,
"maxAutoscaleSeats": null,
"maxAutoscaleSmSeats": null,
"maxAutoscaleSmServiceAccounts": null,
"secretsManagerPlan": null,
"smSeats": null,
"smServiceAccounts": null,
"billingEmail": self.billing_email,
"planType": 6, // Custom plan
"usersGetPremium": true,
@ -252,6 +267,15 @@ impl UserOrganization {
}
false
}
/// HACK: Convert the manager type to a custom type
/// It will be converted back on other locations
pub fn type_manager_as_custom(&self) -> i32 {
match self.atype {
3 => 4,
_ => self.atype,
}
}
}
impl OrganizationApiKey {
@ -356,17 +380,21 @@ impl UserOrganization {
pub async fn to_json(&self, conn: &mut DbConn) -> Value {
let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap();
// HACK: Convert the manager type to a custom type
// It will be converted back on other locations
let user_org_type = self.type_manager_as_custom();
let permissions = json!({
// TODO: Add support for Custom User Roles
// TODO: Add full support for Custom User Roles
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
// Currently we use the custom role as a manager role and link the 3 Collection roles to mimic the access_all permission
"accessEventLogs": false,
"accessImportExport": false,
"accessReports": false,
"createNewCollections": false,
"editAnyCollection": false,
"deleteAnyCollection": false,
"editAssignedCollections": false,
"deleteAssignedCollections": false,
// If the following 3 Collection roles are set to true a custom user has access all permission
"createNewCollections": user_org_type == 4 && self.access_all,
"editAnyCollection": user_org_type == 4 && self.access_all,
"deleteAnyCollection": user_org_type == 4 && self.access_all,
"manageGroups": false,
"managePolicies": false,
"manageSso": false, // Not supported
@ -398,9 +426,9 @@ impl UserOrganization {
"ssoBound": false, // Not supported
"useSso": false, // Not supported
"useKeyConnector": false,
"useSecretsManager": false,
"useSecretsManager": false, // Not supported (Not AGPLv3 Licensed)
"usePasswordManager": true,
"useCustomPermissions": false,
"useCustomPermissions": true,
"useActivateAutofillPolicy": false,
"organizationUserId": self.uuid,
@ -417,9 +445,11 @@ impl UserOrganization {
"familySponsorshipValidUntil": null,
"familySponsorshipToDelete": null,
"accessSecretsManager": false,
"limitCollectionCreationDeletion": false, // This should be set to true only when we can handle roles like createNewCollections
"limitCollectionCreation": true,
"limitCollectionCreationDeletion": true,
"limitCollectionDeletion": true,
"allowAdminAccessToAllCollectionItems": true,
"flexibleCollections": false,
"userIsManagedByOrganization": false, // Means not managed via the Members UI, like SSO
"permissions": permissions,
@ -429,7 +459,7 @@ impl UserOrganization {
"userId": self.user_uuid,
"key": self.akey,
"status": self.status,
"type": self.atype,
"type": user_org_type,
"enabled": true,
"object": "profileOrganization",
@ -516,24 +546,34 @@ impl UserOrganization {
Vec::with_capacity(0)
};
let permissions = json!({
// TODO: Add support for Custom User Roles
// HACK: Convert the manager type to a custom type
// It will be converted back on other locations
let user_org_type = self.type_manager_as_custom();
// HACK: Only return permissions if the user is of type custom and has access_all
// Else Bitwarden will assume the defaults of all false
let permissions = if user_org_type == 4 && self.access_all {
json!({
// TODO: Add full support for Custom User Roles
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
// Currently we use the custom role as a manager role and link the 3 Collection roles to mimic the access_all permission
"accessEventLogs": false,
"accessImportExport": false,
"accessReports": false,
"createNewCollections": false,
"editAnyCollection": false,
"deleteAnyCollection": false,
"editAssignedCollections": false,
"deleteAssignedCollections": false,
// If the following 3 Collection roles are set to true a custom user has access all permission
"createNewCollections": true,
"editAnyCollection": true,
"deleteAnyCollection": true,
"manageGroups": false,
"managePolicies": false,
"manageSso": false, // Not supported
"manageUsers": false,
"manageResetPassword": false,
"manageScim": false // Not supported (Not AGPLv3 Licensed)
});
})
} else {
json!(null)
};
json!({
"id": self.uuid,
@ -546,7 +586,7 @@ impl UserOrganization {
"collections": collections,
"status": status,
"type": self.atype,
"type": user_org_type,
"accessAll": self.access_all,
"twoFactorEnabled": twofactor_enabled,
"resetPasswordEnrolled": self.reset_password_key.is_some(),
@ -608,6 +648,29 @@ impl UserOrganization {
"object": "organizationUserDetails",
})
}
pub async fn to_json_mini_details(&self, conn: &mut DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
// Because Bitwarden wants the status to be -1 for revoked users we need to catch that here.
// We subtract/add a number so we can restore/activate the user to it's previous state again.
let status = if self.status < UserOrgStatus::Revoked as i32 {
UserOrgStatus::Revoked as i32
} else {
self.status
};
json!({
"id": self.uuid,
"userId": self.user_uuid,
"type": self.type_manager_as_custom(), // HACK: Convert the manager type to a custom type
"status": status,
"name": user.name,
"email": user.email,
"object": "organizationUserUserMiniDetails",
})
}
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn).await;
@ -1015,5 +1078,6 @@ mod tests {
assert!(UserOrgType::Owner > UserOrgType::Admin);
assert!(UserOrgType::Admin > UserOrgType::Manager);
assert!(UserOrgType::Manager > UserOrgType::User);
assert!(UserOrgType::Manager == UserOrgType::from_str("4").unwrap());
}
}

2
src/static/scripts/admin_users.js

@ -152,7 +152,7 @@ const ORG_TYPES = {
"name": "User",
"bg": "blue"
},
"3": {
"4": {
"name": "Manager",
"bg": "green"
},

55
src/static/templates/scss/vaultwarden.scss.hbs

@ -42,12 +42,6 @@ label[for^="ownedBusiness"] {
@extend %vw-hide;
}
/* Hide the radio button and label for the `Custom` org user type */
#userTypeCustom,
label[for^="userTypeCustom"] {
@extend %vw-hide;
}
/* Hide Business Name */
app-org-account form div bit-form-field.tw-block:nth-child(3) {
@extend %vw-hide;
@ -58,42 +52,77 @@ app-organization-plans > form > bit-section:nth-child(2) {
@extend %vw-hide;
}
/* Hide Collection Management Form */
app-org-account form.ng-untouched:nth-child(6) {
@extend %vw-hide;
}
/* Hide 'Member Access' Report Card from Org Reports */
app-org-reports-home > app-report-list > div.tw-inline-grid > div:nth-child(6) {
@extend %vw-hide;
}
/* Hide Device Verification form at the Two Step Login screen */
app-security > app-two-factor-setup > form {
@extend %vw-hide;
}
/* Hide unsupported Custom Role options */
bit-dialog div.tw-ml-4:has(bit-form-control input),
bit-dialog div.tw-col-span-4:has(input[formcontrolname*="access"], input[formcontrolname*="manage"]) {
@extend %vw-hide;
}
/* Hide Log in with passkey */
app-login div.tw-flex:nth-child(4) {
@extend %vw-hide;
}
/* Change collapsed menu icon to Vaultwarden */
bit-nav-logo bit-nav-item a:before {
content: "";
background-image: url("../images/icon-white.svg");
background-repeat: no-repeat;
background-position: center center;
height: 32px;
display: block;
}
bit-nav-logo bit-nav-item .bwi-shield {
@extend %vw-hide;
}
/**** END Static Vaultwarden Changes ****/
/**** START Dynamic Vaultwarden Changes ****/
{{#if signup_disabled}}
/* Hide the register link on the login screen */
app-frontend-layout > app-login > form > div > div > div > p {
app-login form div + div + div + div + hr,
app-login form div + div + div + div + hr + p {
@extend %vw-hide;
}
{{/if}}
/* Hide `Email` 2FA if mail is not enabled */
{{#unless mail_enabled}}
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(5) {
/* Hide `Email` 2FA if mail is not enabled */
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(1) {
@extend %vw-hide;
}
{{/unless}}
/* Hide `YubiKey OTP security key` 2FA if it is not enabled */
{{#unless yubico_enabled}}
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(2) {
/* Hide `YubiKey OTP security key` 2FA if it is not enabled */
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(4) {
@extend %vw-hide;
}
{{/unless}}
/* Hide Emergency Access if not allowed */
{{#unless emergency_access_allowed}}
/* Hide Emergency Access if not allowed */
bit-nav-item[route="settings/emergency-access"] {
@extend %vw-hide;
}
{{/unless}}
/* Hide Sends if not allowed */
{{#unless sends_allowed}}
/* Hide Sends if not allowed */
bit-nav-item[route="sends"] {
@extend %vw-hide;
}

Loading…
Cancel
Save