diff --git a/.env.template b/.env.template index ceb7015392..23d4fc29a9 100644 --- a/.env.template +++ b/.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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7da6b91b0c..74f9c44c14 100644 --- a/.github/workflows/build.yml +++ b/.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. diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 35bb343262..787feeec92 100644 --- a/.github/workflows/hadolint.yml +++ b/.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: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93b8919dbe..9169732fa5 100644 --- a/.github/workflows/release.yml +++ b/.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 diff --git a/Cargo.lock b/Cargo.lock index 311fa43b08..b4c19eaece 100644 --- a/Cargo.lock +++ b/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", ] diff --git a/Cargo.toml b/Cargo.toml index f739145b48..a5c320d8e0 100644 --- a/Cargo.toml +++ b/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" diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 6896c3dd47..ccbe622000 100644 --- a/docker/DockerSettings.yaml +++ b/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 diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 77915fd8ca..2c7be7da28 100644 --- a/docker/Dockerfile.alpine +++ b/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 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 69404a2ec0..a4f390919f 100644 --- a/docker/Dockerfile.debian +++ b/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 diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 2f9e2609f6..a91d77df84 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -399,7 +399,12 @@ async fn post_password(data: Json, 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; diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 122bf44fbb..9ce2a29790 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -18,7 +18,7 @@ pub use sends::purge_sends; pub fn routes() -> Vec { 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 { + // 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 { let domain = crate::CONFIG.domain(); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index f315853699..6f404b5604 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -48,6 +48,7 @@ pub fn routes() -> Vec { 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 { 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 { 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 = 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 = 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 = UserOrganization::find_confirmed_by_org(org_id, &mut conn) + .await + .into_iter() + .map(|uo| (uo.uuid, uo.atype)) + .collect(); + let users: Vec = 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>, #[serde(default)] access_all: bool, + #[serde(default)] + permissions: HashMap, } #[post("/organizations//users/invite", data = "")] async fn send_invite(org_id: &str, data: Json, 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, 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, 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, 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, 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//users/?")] +#[get("/organizations//users/mini-details", rank = 1)] +async fn get_org_user_mini_details(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { + 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//users/?", rank = 2)] async fn get_user( org_id: &str, org_user_id: &str, @@ -1282,6 +1356,8 @@ struct EditUserData { groups: Option>, #[serde(default)] access_all: bool, + #[serde(default)] + permissions: HashMap, } #[put("/organizations//users/", 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 { Json(_empty_data_json()) } +#[get("/organizations/<_org_id>/billing/metadata")] +fn get_billing_metadata(_org_id: &str, _headers: Headers) -> Json { + // 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, } +/// This function seems to be deprected +/// It is only used with older directory connectors +/// TODO: Cleanup Tech debt #[post("/organizations//import", data = "")] async fn import(org_id: &str, data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { let data = data.into_inner(); @@ -1981,23 +2082,10 @@ async fn import(org_id: &str, data: Json, 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, 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//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//groups/")] +#[get("/organizations//groups/", 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() { diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 3b3e74cb94..f5f92e6211 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -52,6 +52,7 @@ async fn ldap_import(data: Json, 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, 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, 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, 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:?} ")); + } } } } diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 64cc6486c2..57d7792692 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -16,7 +16,7 @@ use crate::{ pub use crate::config::CONFIG; pub fn routes() -> Vec { - routes![generate_authenticator, activate_authenticator, activate_authenticator_put,] + routes![generate_authenticator, activate_authenticator, activate_authenticator_put, disable_authenticator] } #[post("/two-factor/get-authenticator", 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 = "")] +async fn disable_authenticator(data: Json, 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" + }))) +} diff --git a/src/api/web.rs b/src/api/web.rs index a96d7e2a16..edbffbbddb 100644 --- a/src/api/web.rs +++ b/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> { #[get("/css/vaultwarden.css")] fn vaultwarden_css() -> Cached> { - // 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 = 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::().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 = 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::().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()), diff --git a/src/config.rs b/src/config.rs index 22021238b2..3650bb30b6 100644 --- a/src/config.rs +++ b/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 = 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 = 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 = 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) +); diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 048cb4d13d..782ae69983 100644 --- a/src/db/models/cipher.rs +++ b/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 - type_data_json["uri"] = Value::Null; + // 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::() { + 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(); diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index a26f22c72e..907aebf7a8 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -511,7 +511,10 @@ impl CollectionUser { }} } - pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_organization_swap_user_uuid_with_org_user_uuid( + org_uuid: &str, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_collections::table .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) diff --git a/src/db/models/group.rs b/src/db/models/group.rs index d9a08970b6..84c2727a2d 100644 --- a/src/db/models/group.rs +++ b/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 = 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(); diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 15f009918f..c8c1d3a459 100644 --- a/src/db/models/organization.rs +++ b/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 - // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role - "accessEventLogs": false, - "accessImportExport": false, - "accessReports": false, - "createNewCollections": false, - "editAnyCollection": false, - "deleteAnyCollection": false, - "editAssignedCollections": false, - "deleteAssignedCollections": false, - "manageGroups": false, - "managePolicies": false, - "manageSso": false, // Not supported - "manageUsers": false, - "manageResetPassword": false, - "manageScim": false // Not supported (Not AGPLv3 Licensed) - }); + // 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, + // 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()); } } diff --git a/src/static/scripts/admin_users.js b/src/static/scripts/admin_users.js index c246252156..54fdedf272 100644 --- a/src/static/scripts/admin_users.js +++ b/src/static/scripts/admin_users.js @@ -152,7 +152,7 @@ const ORG_TYPES = { "name": "User", "bg": "blue" }, - "3": { + "4": { "name": "Manager", "bg": "green" }, diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 3fc3e70ed2..42c4d8dce7 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/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; }