Compare commits

..

39 Commits

Author SHA1 Message Date
Mathijs van Veluw
ef7835d1b0 Sync with Upstream (#5798)
* WIP Sync with Upstream

WIP on syncing API Responses with upstream.
This to prevent issues with new clients, and find possible current issues like members, collections, groups etc..

Signed-off-by: BlackDex <black.dex@gmail.com>

* More API Response fixes

- Some 2fa checks
- Some org checks
- Reconfigured the experimental flags and noted which are deprecated
  Also removed some hard-coded defaults.
- Updated crates

Signed-off-by: BlackDex <black.dex@gmail.com>

* Add avatar color to emergency access api

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix spelling and some crate updates

Signed-off-by: BlackDex <black.dex@gmail.com>

* Use PushId and always generate the PushId

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix clippy lints

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix several Push issues and API's

Signed-off-by: BlackDex <black.dex@gmail.com>

* Check if push_uuid is empty and generate when needed

Signed-off-by: BlackDex <black.dex@gmail.com>

* Updated some comments and removed old export format

Signed-off-by: BlackDex <black.dex@gmail.com>

* cargo update

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix bulk edit Fixes #5737

Signed-off-by: BlackDex <black.dex@gmail.com>

* Send an email when an account exists already

When you want to change your email address into an account which already exists, upstream sends an email to the existing account.
Lets do the same.

Kinda fixes #5630

Signed-off-by: BlackDex <black.dex@gmail.com>

* Update 2fa removal/revoke email

Signed-off-by: BlackDex <black.dex@gmail.com>

* Allow col managers to import

This commit adds functionality to allow users with manage access to a collection, or managers with all access to import into an organization.

Fixes #5592

Signed-off-by: BlackDex <black.dex@gmail.com>

* Filter deprected flags and only return active flags

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix grammer

Signed-off-by: BlackDex <black.dex@gmail.com>

* Rename Small to Compact

Signed-off-by: BlackDex <black.dex@gmail.com>

* Rebase with upstream and fix conflicts

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-05-26 21:00:59 +02:00
Mathijs van Veluw
3a44dc963b Update admin interface (#5880)
- Updated Backend Admin dependencies
- Fixed NTP time by using CloudFlare trace - Fixes #5797
- Fixed web-vault version check = Fixes #5761
- Fixed an issue with the css not hiding the 'Create Account' link.
  There were no braces around the function call.
  Also added a hide for newer web-vault versions as it still causes confusion with the cached /api/config.

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-05-26 20:37:50 +02:00
Timshel
a039e227c7 web-client now request email 2fa (#5871) 2025-05-26 20:24:30 +02:00
Timshel
602b18fdd6 Remove old client version check (#5874) 2025-05-23 17:24:03 +02:00
Timshel
bf04c64759 Toggle providers using class (#5832) 2025-05-16 18:54:44 +02:00
Timshel
2f1d86b7f1 remove Hide Business scss rules (#5855) 2025-05-16 18:53:01 +02:00
moodejb123
ff97bcfdda Add totp menu feature flag (#5850) 2025-05-16 18:52:00 +02:00
Mathijs van Veluw
73f2441d1a Update Rust, Crates and Web-Vault (#5860)
- Updated web-vault to v2025.5.0
- Updated Rust to v1.87.0
- Updated all the crates
- Replaced yubico with yubico_ng
- Fixed several new (nightly) clippy lints

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-05-16 18:49:43 +02:00
Helmut K. C. Tessarek
ad8484a2d5 feat: add ip address in logs when email 2fa token is invalid or not available (#5779)
* Update email.rs

Add ip_src on logs when email 2fa token is invalid or not available
Changes for fail2ban purposes

* Update email.rs

removed current_time

* fix: compile error

---------

Co-authored-by: setsecurity <set.ghost@gmail.com>
2025-05-12 19:27:43 +02:00
Josh
9813e480c0 Fix minimum Android version for self-host email alias feature flags (#5802)
* Fix minimum Android version for self-host email alias feature flags

* Update documentation for iOS
2025-05-12 19:25:46 +02:00
Timshel
bfe172702a Fix Yubico toggle (#5833) 2025-05-12 19:24:27 +02:00
Timshel
df42b6d6b0 Fix Yubico toggle (#5833) 2025-05-12 19:24:12 +02:00
Helmut K. C. Tessarek
2697fe8aba feat: add feature flag export-attachments (#5784) 2025-05-01 17:40:26 +02:00
Stefan Melmuk
674e444d67 respond with cipher json when deleting attachments (#5823) 2025-05-01 17:28:23 +02:00
Timshel
0d16da440d On member invite and edit access_all is not sent anymore (#5673)
* On member invite and edit access_all is not sent anymore

* Use MembershipType ordering for access_all check

Fixes #5711
2025-04-16 17:52:26 +02:00
Mathijs van Veluw
66cf179bca Updates and general fixes (#5762)
Updated all the crates to the latest version.
We can unpin mimalloc, since the musl issues have been fixed
Also fix a RUSTSEC https://osv.dev/vulnerability/RUSTSEC-2025-0023 for tokio

Fixed some clippy lints reported by nightly.

Ensure lints and are also run on the macro crate.
This resulted in some lints being triggered, which I fixed.

Updated some GHA uses.

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-04-09 21:21:10 +02:00
Mathijs van Veluw
025bb90f8f Fix debian docker building (#5752)
In previous attempts to get mysqlclient-sys to build and work I added some extra build variables.
These are not needed if you configure pkg-config correctly.
The same goes for OpenSSL btw.

This PR configures the pkg-config in the right way and allows the crates to build using the right lib paths automatically.
Because of this change also the lib/include paths were not needed anymore for some architectures, except for i386.

Also updated crates again.

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-04-05 17:58:32 +02:00
Mathijs van Veluw
d5039d9c17 Add Docker Templates pre-commit check (#5749)
Added the same check as done via GitHub Actions to check template changes to the pre-commit checks.
This should catch these mistakes before they are commited and pushed.

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-04-04 19:02:19 +02:00
Daniel García
e7c796a660 Verify templates in CI (#5748)
* Verify templates in CI

* No need to install packages

* Remove unnecessary fetch depth
2025-04-04 18:14:19 +02:00
Daniel
bbbd2f6d15 Update Rust to 1.86.0 (#5744)
- also raise MSRV to 1.84.0

- fix `Dockerfile` template
- remove no longed needed `-vvv` argument for `cargo build`
2025-04-04 18:04:36 +02:00
Mathijs van Veluw
a2d7895586 Really fix building (#5745)
Signed-off-by: BlackDex <black.dex@gmail.com>
2025-04-04 16:53:09 +02:00
Mathijs van Veluw
8a0cb1137e Fix mysqlclient-sys building (#5743)
Because of some issues with mysqlclient we need to use buildtime bindgen.
This also needed some extra environment variables to point the bindgen to the correct files and correct version.

Also update some other crates.

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-04-04 16:37:57 +02:00
Timshel
f960bf59bb Fix invited user registration without SMTP (#5712) 2025-04-04 13:54:28 +02:00
Mathijs van Veluw
3a1f1bae00 Update deps and web-vault (#5742)
- Updated crates
  Pinned mimalloc, since it has issues with musl
- Updated web-vault to v2025.3.1
- Updated bootstrap

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-04-04 12:18:09 +02:00
Mathijs van Veluw
8dfe805954 Update Rust, Crates and other deps (#5709)
- Updated Rust to v1.85.1
- Updated crates and fixed breaking changes
- Updated datatables js
- Updated GitHub Actions

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-03-19 17:39:53 +01:00
Mathijs van Veluw
07b869b3ef Some fixes for the new web-vault and updates (#5703)
- Added a new org policy
- Some new lint fixes
- Crate updates
  Switched to `pastey`, since `paste` is unmaintained.

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-03-17 23:02:02 +01:00
Daniel García
2a18665288 Implement new registration flow with email verification (#5215)
* Implement registration with required verified email

* Optional name, emergency access, and signups_allowed

* Implement org invite, remove unneeded invite accept

* fix invitation logic for new registration flow (#5691)

* fix invitation logic for new registration flow

* clarify email_2fa_enforce_on_verified_invite

---------

Co-authored-by: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com>
2025-03-17 16:28:01 +01:00
Josh
71952a4ab5 Add AnonAddy/SimpleLogin self host feature flag (#5694)
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
2025-03-15 19:57:04 +01:00
Ben Sherman
994d157064 Add support for mutual-tls feature flag (#5698)
* Add support for mutual-tls feature flag

* Fix formatting

---------

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
2025-03-15 19:46:42 +01:00
Timshel
1dae6093c9 Use subtle to replace deprecated ring::constant_time::verify_slices_are_equal (#5680) 2025-03-15 19:33:17 +01:00
Daniel
6edceb5f7a Update Rust to 1.85.0 (#5634)
- also update the crates
2025-02-24 12:12:34 +01:00
Stefan Melmuk
359a4a088a allow CLI to upload files with truncated filenames (#5618)
due to a bug in the CLI the filename in the form-data is not complete if
the encrypted filename happens to contain a /
2025-02-19 10:40:59 +01:00
Mathijs van Veluw
3baffeee9a Fix db issues with Option<> values and upd crates (#5594)
Some tables were lacking an option to convert Option<> to NULL.
This commit will fix that.

Also updated the crates to the latest version available.
2025-02-14 17:58:57 +01:00
Daniel
d5c353427d Update crates & fix CVE-2025-25188 (#5576) 2025-02-12 10:21:12 +01:00
Mathijs van Veluw
1f868b8d22 Show assigned collections on member edit (#5556)
Because we were using the `has_full_access()` function we did not returned assigned collections for an owner/admin even if the did not have the `access_all` flag set.
This commit will change that to use the `access_all` flag instead, and return assigned collections too.

While saving a member and having it assigned collections would still save those rights, and it was also visible in the collection management, it wasn't at the member it self.
So, it did work, but was not visible.

Fixes #5554
Fixes #5555

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-02-07 22:33:11 +01:00
Mathijs van Veluw
8d1df08b81 Fix icon redirect not working on desktop (#5536)
* Fix icon redirect not working on desktop

We also need to exclude the header in case we do an external_icon call.

Fixes #5535

Signed-off-by: BlackDex <black.dex@gmail.com>

* Add informational comments to the icon_external function

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix spelling/grammar

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-02-04 13:20:32 +01:00
Stefan Melmuk
3b6bccde97 add bulk-access endpoint for collections (#5542) 2025-02-04 09:42:02 +01:00
Daniel
d2b36642a6 Update crates & fix CVE-2025-24898 (#5538) 2025-02-04 01:01:06 +01:00
Mathijs van Veluw
a02fb0fd24 Update workflows and enhance security (#5537)
This commit updates the workflow files and also fixes some security issues which were reported by using zizmor https://github.com/woodruffw/zizmor

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-02-04 00:33:43 +01:00
76 changed files with 3167 additions and 1787 deletions

View File

@@ -229,7 +229,8 @@
# SIGNUPS_ALLOWED=true
## Controls if new users need to verify their email address upon registration
## Note that setting this option to true prevents logins until the email address has been verified!
## On new client versions, this will require the user to verify their email at signup time.
## On older clients, it will require the user to verify their email before they can log in.
## The welcome email will include a verification link, and login attempts will periodically
## trigger another verification email to be sent.
# SIGNUPS_VERIFY=false
@@ -343,16 +344,17 @@
## Client Settings
## Enable experimental feature flags for clients.
## This is a comma-separated list of flags, e.g. "flag1,flag2,flag3".
## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled!
##
## The following flags are available:
## - "autofill-overlay": Add an overlay menu to form fields for quick access to credentials.
## - "autofill-v2": Use the new autofill implementation.
## - "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)
## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension.
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0)
## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
## - "mutual-tls": Enable the use of mutual TLS on Android (Client >= 2025.2.0)
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
## Require new device emails. When a user logs in an email is required to be sent.
@@ -486,7 +488,7 @@
## Maximum attempts before an email token is reset and a new email will need to be sent.
# EMAIL_ATTEMPTS_LIMIT=3
##
## Setup email 2FA regardless of any organization policy
## Setup email 2FA on registration regardless of any organization policy
# EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false
## Automatically setup email 2FA as fallback provider when needed
# EMAIL_2FA_AUTO_FALLBACK=false

2
.github/CODEOWNERS vendored
View File

@@ -1,3 +1,5 @@
/.github @dani-garcia @BlackDex
/.github/** @dani-garcia @BlackDex
/.github/CODEOWNERS @dani-garcia @BlackDex
/.github/workflows/** @dani-garcia @BlackDex
/SECURITY.md @dani-garcia @BlackDex

View File

@@ -1,4 +1,5 @@
name: Build
permissions: {}
on:
push:
@@ -13,6 +14,7 @@ on:
- "diesel.toml"
- "docker/Dockerfile.j2"
- "docker/DockerSettings.yaml"
pull_request:
paths:
- ".github/workflows/build.yml"
@@ -28,13 +30,17 @@ on:
jobs:
build:
name: Build and Test ${{ matrix.channel }}
permissions:
actions: write
contents: read
# We use Ubuntu 22.04 here because this matches the library versions used within the Debian docker containers
runs-on: ubuntu-22.04
timeout-minutes: 120
# Make warnings errors, this is to prevent warnings slipping through.
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
env:
RUSTFLAGS: "-D warnings"
RUSTFLAGS: "-Dwarnings"
strategy:
fail-fast: false
matrix:
@@ -42,20 +48,19 @@ jobs:
- "rust-toolchain" # The version defined in rust-toolchain
- "msrv" # The supported MSRV
name: Build and Test ${{ matrix.channel }}
steps:
# Checkout the repo
- name: "Checkout"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
# End Checkout the repo
# Install dependencies
- name: "Install dependencies Ubuntu"
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config
# End Install dependencies
# Checkout the repo
- name: "Checkout"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
persist-credentials: false
fetch-depth: 0
# End Checkout the repo
# Determine rust-toolchain version
- name: Init Variables
@@ -75,7 +80,7 @@ jobs:
# Only install the clippy and rustfmt components on the default rust-toolchain
- name: "Install rust-toolchain version"
uses: dtolnay/rust-toolchain@a54c7afa936fefeb4456b2dd8068152669aa8203 # master @ Dec 14, 2024, 5:49 AM GMT+1
uses: dtolnay/rust-toolchain@56f84321dbccf38fb67ce29ab63e4754056677e0 # master @ Mar 18, 2025, 8:14 PM GMT+1
if: ${{ matrix.channel == 'rust-toolchain' }}
with:
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
@@ -85,7 +90,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@a54c7afa936fefeb4456b2dd8068152669aa8203 # master @ Dec 14, 2024, 5:49 AM GMT+1
uses: dtolnay/rust-toolchain@56f84321dbccf38fb67ce29ab63e4754056677e0 # master @ Mar 18, 2025, 8:14 PM GMT+1
if: ${{ matrix.channel != 'rust-toolchain' }}
with:
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
@@ -93,11 +98,13 @@ jobs:
# Set the current matrix toolchain version as default
- name: "Set toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default"
env:
RUST_TOOLCHAIN: ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}
run: |
# Remove the rust-toolchain.toml
rm rust-toolchain.toml
# Set the default
rustup default ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}
rustup default "${RUST_TOOLCHAIN}"
# Show environment
- name: "Show environment"
@@ -108,7 +115,7 @@ jobs:
# Enable Rust Caching
- name: Rust Caching
uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
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.
@@ -161,7 +168,7 @@ jobs:
id: clippy
if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}
run: |
cargo clippy --features sqlite,mysql,postgresql,enable_mimalloc -- -D warnings
cargo clippy --features sqlite,mysql,postgresql,enable_mimalloc
# End Run cargo clippy
@@ -178,22 +185,31 @@ jobs:
# This is useful so all test/clippy/fmt actions are done, and they can all be addressed
- name: "Some checks failed"
if: ${{ failure() }}
env:
TEST_DB_M_L: ${{ steps.test_sqlite_mysql_postgresql_mimalloc_logger.outcome }}
TEST_DB_M: ${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}
TEST_DB: ${{ steps.test_sqlite_mysql_postgresql.outcome }}
TEST_SQLITE: ${{ steps.test_sqlite.outcome }}
TEST_MYSQL: ${{ steps.test_mysql.outcome }}
TEST_POSTGRESQL: ${{ steps.test_postgresql.outcome }}
CLIPPY: ${{ steps.clippy.outcome }}
FMT: ${{ steps.formatting.outcome }}
run: |
echo "### :x: Checks Failed!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "|Job|Status|" >> $GITHUB_STEP_SUMMARY
echo "|---|------|" >> $GITHUB_STEP_SUMMARY
echo "|test (sqlite,mysql,postgresql,enable_mimalloc,query_logger)|${{ steps.test_sqlite_mysql_postgresql_mimalloc_logger.outcome }}|" >> $GITHUB_STEP_SUMMARY
echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}|" >> $GITHUB_STEP_SUMMARY
echo "|test (sqlite,mysql,postgresql)|${{ steps.test_sqlite_mysql_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
echo "|test (sqlite)|${{ steps.test_sqlite.outcome }}|" >> $GITHUB_STEP_SUMMARY
echo "|test (mysql)|${{ steps.test_mysql.outcome }}|" >> $GITHUB_STEP_SUMMARY
echo "|test (postgresql)|${{ steps.test_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
echo "|clippy (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.clippy.outcome }}|" >> $GITHUB_STEP_SUMMARY
echo "|fmt|${{ steps.formatting.outcome }}|" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Please check the failed jobs and fix where needed." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### :x: Checks Failed!" >> "${GITHUB_STEP_SUMMARY}"
echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "|Job|Status|" >> "${GITHUB_STEP_SUMMARY}"
echo "|---|------|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (sqlite,mysql,postgresql,enable_mimalloc,query_logger)|${TEST_DB_M_L}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${TEST_DB_M}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (sqlite,mysql,postgresql)|${TEST_DB}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (sqlite)|${TEST_SQLITE}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (mysql)|${TEST_MYSQL}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (postgresql)|${TEST_POSTGRESQL}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|clippy (sqlite,mysql,postgresql,enable_mimalloc)|${CLIPPY}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|fmt|${FMT}|" >> "${GITHUB_STEP_SUMMARY}"
echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "Please check the failed jobs and fix where needed." >> "${GITHUB_STEP_SUMMARY}"
echo "" >> "${GITHUB_STEP_SUMMARY}"
exit 1
@@ -202,5 +218,5 @@ jobs:
- name: "All checks passed"
if: ${{ success() }}
run: |
echo "### :tada: Checks Passed!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### :tada: Checks Passed!" >> "${GITHUB_STEP_SUMMARY}"
echo "" >> "${GITHUB_STEP_SUMMARY}"

28
.github/workflows/check-templates.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Check templates
permissions: {}
on: [ push, pull_request ]
jobs:
docker-templates:
permissions:
contents: read
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
# Checkout the repo
- name: "Checkout"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
persist-credentials: false
# End Checkout the repo
- name: Run make to rebuild templates
working-directory: docker
run: make
- name: Check for unstaged changes
working-directory: docker
run: git diff --exit-code
continue-on-error: false

View File

@@ -1,24 +1,20 @@
name: Hadolint
permissions: {}
on: [
push,
pull_request
]
on: [ push, pull_request ]
jobs:
hadolint:
name: Validate Dockerfile syntax
permissions:
contents: read
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
# Checkout the repo
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
# End Checkout the repo
steps:
# Start Docker Buildx
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.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:
@@ -37,6 +33,12 @@ jobs:
env:
HADOLINT_VERSION: 2.12.0
# End Download hadolint
# Checkout the repo
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
persist-credentials: false
# End Checkout the repo
# Test Dockerfiles with hadolint
- name: Run hadolint

View File

@@ -1,4 +1,5 @@
name: Release
permissions: {}
on:
push:
@@ -6,17 +7,23 @@ on:
- main
tags:
- '*'
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
- '[1-2].[0-9]+.[0-9]+'
jobs:
# https://github.com/marketplace/actions/skip-duplicate-actions
# Some checks to determine if we need to continue with building a new docker.
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already.
skip_check:
runs-on: ubuntu-24.04
# Only run this in the upstream repo and not on forks
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
name: Cancel older jobs when running
permissions:
actions: write
runs-on: ubuntu-24.04
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- name: Skip Duplicates Actions
id: skip_check
@@ -27,6 +34,9 @@ jobs:
if: ${{ github.ref_type == 'branch' }}
docker-build:
needs: skip_check
if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }}
name: Build Vaultwarden containers
permissions:
packages: write
contents: read
@@ -34,8 +44,6 @@ jobs:
id-token: write
runs-on: ubuntu-24.04
timeout-minutes: 120
needs: skip_check
if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }}
# Start a local docker registry to extract the compiled binaries to upload as artifacts and attest them
services:
registry:
@@ -61,37 +69,42 @@ jobs:
base_image: ["debian","alpine"]
steps:
# Checkout the repo
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
fetch-depth: 0
- name: Initialize QEMU binfmt support
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
with:
platforms: "arm64,arm"
# Start Docker Buildx
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.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:
cache-binary: false
buildkitd-config-inline: |
[worker.oci]
max-parallelism = 2
driver-opts: |
network=host
# Checkout the repo
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
# We need fetch-depth of 0 so we also get all the tag metadata
with:
persist-credentials: false
fetch-depth: 0
# Determine Base Tags and Source Version
- name: Determine Base Tags and Source Version
shell: bash
env:
REF_TYPE: ${{ github.ref_type }}
run: |
# Check which main tag we are going to build determined by github.ref_type
if [[ "${{ github.ref_type }}" == "tag" ]]; then
# Check which main tag we are going to build determined by ref_type
if [[ "${REF_TYPE}" == "tag" ]]; then
echo "BASE_TAGS=latest,${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_ENV}"
elif [[ "${{ github.ref_type }}" == "branch" ]]; then
elif [[ "${REF_TYPE}" == "branch" ]]; then
echo "BASE_TAGS=testing" | tee -a "${GITHUB_ENV}"
fi
@@ -107,7 +120,7 @@ jobs:
# Login to Docker Hub
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -116,12 +129,14 @@ jobs:
- name: Add registry for DockerHub
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
shell: bash
env:
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
run: |
echo "CONTAINER_REGISTRIES=${{ vars.DOCKERHUB_REPO }}" | tee -a "${GITHUB_ENV}"
echo "CONTAINER_REGISTRIES=${DOCKERHUB_REPO}" | tee -a "${GITHUB_ENV}"
# Login to GitHub Container Registry
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -131,12 +146,14 @@ jobs:
- name: Add registry for ghcr.io
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
shell: bash
env:
GHCR_REPO: ${{ vars.GHCR_REPO }}
run: |
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${{ vars.GHCR_REPO }}" | tee -a "${GITHUB_ENV}"
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${GHCR_REPO}" | tee -a "${GITHUB_ENV}"
# Login to Quay.io
- name: Login to Quay.io
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
@@ -146,17 +163,22 @@ jobs:
- name: Add registry for Quay.io
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
shell: bash
env:
QUAY_REPO: ${{ vars.QUAY_REPO }}
run: |
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${{ vars.QUAY_REPO }}" | tee -a "${GITHUB_ENV}"
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${QUAY_REPO}" | tee -a "${GITHUB_ENV}"
- name: Configure build cache from/to
shell: bash
env:
GHCR_REPO: ${{ vars.GHCR_REPO }}
BASE_IMAGE: ${{ matrix.base_image }}
run: |
#
# Check if there is a GitHub Container Registry Login and use it for caching
if [[ -n "${HAVE_GHCR_LOGIN}" ]]; then
echo "BAKE_CACHE_FROM=type=registry,ref=${{ vars.GHCR_REPO }}-buildcache:${{ matrix.base_image }}" | tee -a "${GITHUB_ENV}"
echo "BAKE_CACHE_TO=type=registry,ref=${{ vars.GHCR_REPO }}-buildcache:${{ matrix.base_image }},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
echo "BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}" | tee -a "${GITHUB_ENV}"
echo "BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
else
echo "BAKE_CACHE_FROM="
echo "BAKE_CACHE_TO="
@@ -170,7 +192,7 @@ jobs:
- name: Bake ${{ matrix.base_image }} containers
id: bake_vw
uses: docker/bake-action@5ca506d06f70338a4968df87fd8bfee5cbfb84c7 # v6.0.0
uses: docker/bake-action@4ba453fbc2db7735392b93edf935aaf9b1e8f747 # v6.5.0
env:
BASE_TAGS: "${{ env.BASE_TAGS }}"
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
@@ -189,14 +211,16 @@ jobs:
- name: Extract digest SHA
shell: bash
env:
BAKE_METADATA: ${{ steps.bake_vw.outputs.metadata }}
run: |
GET_DIGEST_SHA="$(jq -r '.["${{ matrix.base_image }}-multi"]."containerimage.digest"' <<< '${{ steps.bake_vw.outputs.metadata }}')"
GET_DIGEST_SHA="$(jq -r '.["${{ matrix.base_image }}-multi"]."containerimage.digest"' <<< "${BAKE_METADATA}")"
echo "DIGEST_SHA=${GET_DIGEST_SHA}" | tee -a "${GITHUB_ENV}"
# Attest container images
- name: Attest - docker.io - ${{ matrix.base_image }}
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
with:
subject-name: ${{ vars.DOCKERHUB_REPO }}
subject-digest: ${{ env.DIGEST_SHA }}
@@ -204,7 +228,7 @@ jobs:
- name: Attest - ghcr.io - ${{ matrix.base_image }}
if: ${{ env.HAVE_GHCR_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
with:
subject-name: ${{ vars.GHCR_REPO }}
subject-digest: ${{ env.DIGEST_SHA }}
@@ -212,7 +236,7 @@ jobs:
- name: Attest - quay.io - ${{ matrix.base_image }}
if: ${{ env.HAVE_QUAY_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
with:
subject-name: ${{ vars.QUAY_REPO }}
subject-digest: ${{ env.DIGEST_SHA }}
@@ -222,11 +246,13 @@ jobs:
# Extract the Alpine binaries from the containers
- name: Extract binaries
shell: bash
env:
REF_TYPE: ${{ github.ref_type }}
run: |
# Check which main tag we are going to build determined by github.ref_type
if [[ "${{ github.ref_type }}" == "tag" ]]; then
# Check which main tag we are going to build determined by ref_type
if [[ "${REF_TYPE}" == "tag" ]]; then
EXTRACT_TAG="latest"
elif [[ "${{ github.ref_type }}" == "branch" ]]; then
elif [[ "${REF_TYPE}" == "branch" ]]; then
EXTRACT_TAG="testing"
fi
@@ -264,31 +290,31 @@ jobs:
# Upload artifacts to Github Actions and Attest the binaries
- name: "Upload amd64 artifact ${{ matrix.base_image }}"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64-${{ matrix.base_image }}
path: vaultwarden-amd64-${{ matrix.base_image }}
- name: "Upload arm64 artifact ${{ matrix.base_image }}"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64-${{ matrix.base_image }}
path: vaultwarden-arm64-${{ matrix.base_image }}
- name: "Upload armv7 artifact ${{ matrix.base_image }}"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7-${{ matrix.base_image }}
path: vaultwarden-armv7-${{ matrix.base_image }}
- name: "Upload armv6 artifact ${{ matrix.base_image }}"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6-${{ matrix.base_image }}
path: vaultwarden-armv6-${{ matrix.base_image }}
- name: "Attest artifacts ${{ matrix.base_image }}"
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
with:
subject-path: vaultwarden-*
# End Upload artifacts to Github Actions

View File

@@ -1,3 +1,6 @@
name: Cleanup
permissions: {}
on:
workflow_dispatch:
inputs:
@@ -9,10 +12,11 @@ on:
schedule:
- cron: '0 1 * * FRI'
name: Cleanup
jobs:
releasecache-cleanup:
name: Releasecache Cleanup
permissions:
packages: write
runs-on: ubuntu-24.04
continue-on-error: true
timeout-minutes: 30

View File

@@ -1,37 +1,42 @@
name: trivy
name: Trivy
permissions: {}
on:
push:
branches:
- main
tags:
- '*'
pull_request:
branches: [ "main" ]
branches:
- main
schedule:
- cron: '08 11 * * *'
permissions:
contents: read
jobs:
trivy-scan:
# Only run this in the master repo and not on forks
# Only run this in the upstream repo and not on forks
# When all forks run this at the same time, it is causing `Too Many Requests` issues
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
name: Check
runs-on: ubuntu-24.04
timeout-minutes: 30
name: Trivy Scan
permissions:
contents: read
security-events: write
actions: read
security-events: write
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
persist-credentials: false
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # v0.29.0
uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # v0.30.0
env:
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1

View File

@@ -1,7 +1,7 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-yaml
- id: check-json
@@ -31,7 +31,7 @@ repos:
language: system
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--"]
types_or: [rust, file]
files: (Cargo.toml|Cargo.lock|rust-toolchain|.*\.rs$)
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
pass_filenames: false
- id: cargo-clippy
name: cargo clippy
@@ -40,5 +40,13 @@ repos:
language: system
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--", "-D", "warnings"]
types_or: [rust, file]
files: (Cargo.toml|Cargo.lock|rust-toolchain|clippy.toml|.*\.rs$)
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
pass_filenames: false
- id: check-docker-templates
name: check docker templates
description: Check if the Docker templates are updated
language: system
entry: sh
args:
- "-c"
- "cd docker && make"

1264
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
workspace = { members = ["macros"] }
[workspace]
members = ["macros"]
[package]
name = "vaultwarden"
version = "1.0.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
edition = "2021"
rust-version = "1.83.0"
rust-version = "1.85.0"
resolver = "2"
repository = "https://github.com/dani-garcia/vaultwarden"
@@ -44,7 +45,7 @@ syslog = "7.0.0"
macros = { path = "./macros" }
# Logging
log = "0.4.25"
log = "0.4.27"
fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] }
tracing = { version = "0.1.41", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
@@ -52,12 +53,12 @@ tracing = { version = "0.1.41", features = ["log"] } # Needed to have lettre and
dotenvy = { version = "0.15.7", default-features = false }
# Lazy initialization
once_cell = "1.20.2"
once_cell = "1.21.3"
# Numerical libraries
num-traits = "0.2.19"
num-derive = "0.4.2"
bigdecimal = "0.4.7"
bigdecimal = "0.4.8"
# Web framework
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
@@ -71,49 +72,50 @@ dashmap = "6.1.0"
# Async futures
futures = "0.3.31"
tokio = { version = "1.43.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
tokio = { version = "1.45.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
# A generic serialization/deserialization framework
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
# A safe, extensible ORM and Query builder
diesel = { version = "2.2.7", features = ["chrono", "r2d2", "numeric"] }
diesel = { version = "2.2.10", features = ["chrono", "r2d2", "numeric"] }
diesel_migrations = "2.2.0"
diesel_logger = { version = "0.4.0", optional = true }
derive_more = { version = "1.0.0", features = ["from", "into", "as_ref", "deref", "display"] }
derive_more = { version = "2.0.1", features = ["from", "into", "as_ref", "deref", "display"] }
diesel-derive-newtype = "2.1.2"
# Bundled/Static SQLite
libsqlite3-sys = { version = "0.31.0", features = ["bundled"], optional = true }
libsqlite3-sys = { version = "0.33.0", features = ["bundled"], optional = true }
# Crypto-related libraries
rand = "0.9.0"
ring = "0.17.8"
rand = "0.9.1"
ring = "0.17.14"
subtle = "2.6.1"
# UUID generation
uuid = { version = "1.12.1", features = ["v4"] }
uuid = { version = "1.17.0", features = ["v4"] }
# Date and time libraries
chrono = { version = "0.4.39", features = ["clock", "serde"], default-features = false }
chrono-tz = "0.10.1"
time = "0.3.37"
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }
chrono-tz = "0.10.3"
time = "0.3.41"
# Job scheduler
job_scheduler_ng = "2.0.5"
job_scheduler_ng = "2.2.0"
# Data encoding library Hex/Base32/Base64
data-encoding = "2.7.0"
data-encoding = "2.9.0"
# JWT library
jsonwebtoken = "9.3.0"
jsonwebtoken = "9.3.1"
# TOTP library
totp-lite = "2.0.1"
# Yubico Library
yubico = { version = "0.12.0", features = ["online-tokio"], default-features = false }
yubico = { package = "yubico_ng", version = "0.13.0", features = ["online-tokio"], default-features = false }
# WebAuthn libraries
webauthn-rs = "0.3.2"
@@ -122,63 +124,60 @@ webauthn-rs = "0.3.2"
url = "2.5.4"
# Email libraries
lettre = { version = "0.11.11", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
lettre = { version = "0.11.16", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
email_address = "0.2.9"
# HTML Template library
handlebars = { version = "6.3.0", features = ["dir_source"] }
handlebars = { version = "6.3.2", features = ["dir_source"] }
# HTTP client (Used for favicons, version check, DUO and HIBP API)
reqwest = { version = "0.12.12", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
hickory-resolver = "0.24.2"
reqwest = { version = "0.12.15", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
hickory-resolver = "0.25.2"
# Favicon extraction libraries
html5gum = "0.7.0"
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
data-url = "0.3.1"
bytes = "1.9.0"
bytes = "1.10.1"
# Cache function results (Used for version check and favicon fetching)
cached = { version = "0.54.0", features = ["async"] }
cached = { version = "0.55.1", features = ["async"] }
# Used for custom short lived cookie jar during favicon extraction
cookie = "0.18.1"
cookie_store = "0.21.1"
# Used by U2F, JWT and PostgreSQL
openssl = "0.10.69"
openssl = "0.10.72"
# CLI argument parsing
pico-args = "0.5.0"
# Macro ident concatenation
paste = "1.0.15"
governor = "0.8.0"
pastey = "0.1.0"
governor = "0.10.0"
# Check client versions for specific features.
semver = "1.0.25"
semver = "1.0.26"
# 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.1"
mimalloc = { version = "0.1.46", features = ["secure"], default-features = false, optional = true }
which = "7.0.3"
# Argon2 library with support for the PHC format
argon2 = "0.5.3"
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
rpassword = "7.3.1"
rpassword = "7.4.0"
# Loading a dynamic CSS Stylesheet
grass_compiler = { version = "0.13.4", default-features = false }
[patch.crates-io]
# Patch yubico to remove duplicate crates of older versions
yubico = { git = "https://github.com/BlackDex/yubico-rs", rev = "00df14811f58155c0f02e3ab10f1570ed3e115c6" }
# Strip debuginfo from the release builds
# The symbols are the provide better panic traces
# The debug symbols are to provide better panic traces
# Also enable fat LTO and use 1 codegen unit for optimizations
[profile.release]
strip = "debuginfo"
@@ -213,7 +212,7 @@ codegen-units = 16
# Linting config
# https://doc.rust-lang.org/rustc/lints/groups.html
[lints.rust]
[workspace.lints.rust]
# Forbid
unsafe_code = "forbid"
non_ascii_idents = "forbid"
@@ -243,11 +242,14 @@ if_let_rescope = "allow"
tail_expr_drop_order = "allow"
# https://rust-lang.github.io/rust-clippy/stable/index.html
[lints.clippy]
[workspace.lints.clippy]
# Warn
dbg_macro = "warn"
todo = "warn"
# Ignore/Allow
result_large_err = "allow"
# Deny
case_sensitive_file_extension_comparisons = "deny"
cast_lossless = "deny"
@@ -278,3 +280,6 @@ unused_async = "deny"
unused_self = "deny"
verbose_file_reads = "deny"
zero_sized_map_values = "deny"
[lints]
workspace = true

View File

@@ -48,8 +48,8 @@ fn main() {
fn run(args: &[&str]) -> Result<String, std::io::Error> {
let out = Command::new(args[0]).args(&args[1..]).output()?;
if !out.status.success() {
use std::io::{Error, ErrorKind};
return Err(Error::new(ErrorKind::Other, "Command not successful"));
use std::io::Error;
return Err(Error::other("Command not successful"));
}
Ok(String::from_utf8(out.stdout).unwrap().trim().to_string())
}

View File

@@ -1,11 +1,11 @@
---
vault_version: "v2025.1.1"
vault_image_digest: "sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918"
vault_version: "v2025.5.0"
vault_image_digest: "sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e"
# 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:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894"
rust_version: 1.84.1 # Rust version to be used
rust_version: 1.87.0 # Rust version to be used
debian_version: bookworm # Debian release name to be used
alpine_version: "3.21" # Alpine version to be used
# For which platforms/architectures will we try to build images

View File

@@ -19,23 +19,23 @@
# - 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:v2025.1.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.1
# [docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918]
# $ docker pull docker.io/vaultwarden/web-vault:v2025.5.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.5.0
# [docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918
# [docker.io/vaultwarden/web-vault:v2025.1.1]
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e
# [docker.io/vaultwarden/web-vault:v2025.5.0]
#
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918 AS vault
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e AS vault
########################## ALPINE BUILD IMAGES ##########################
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
## And for Alpine we define all build images here, they will only be loaded when actually used
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.84.1 AS build_amd64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.84.1 AS build_arm64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.84.1 AS build_armv7
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.84.1 AS build_armv6
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.87.0 AS build_amd64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.87.0 AS build_arm64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.87.0 AS build_armv7
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.87.0 AS build_armv6
########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006

View File

@@ -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:v2025.1.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.1
# [docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918]
# $ docker pull docker.io/vaultwarden/web-vault:v2025.5.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.5.0
# [docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918
# [docker.io/vaultwarden/web-vault:v2025.1.1]
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e
# [docker.io/vaultwarden/web-vault:v2025.5.0]
#
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918 AS vault
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e AS vault
########################## Cross Compile Docker Helper Scripts ##########################
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
@@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:9c207bead753dda9430bd
########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.84.1-slim-bookworm AS build
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.87.0-slim-bookworm AS build
COPY --from=xx / /
ARG TARGETARCH
ARG TARGETVARIANT
@@ -89,24 +89,24 @@ RUN USER=root cargo new --bin /app
WORKDIR /app
# Environment variables for Cargo on Debian based builds
ARG ARCH_OPENSSL_LIB_DIR \
ARCH_OPENSSL_INCLUDE_DIR
ARG TARGET_PKG_CONFIG_PATH
RUN source /env-cargo && \
if xx-info is-cross ; then \
# Some special variables if needed to override some build paths
if [[ -n "${ARCH_OPENSSL_LIB_DIR}" && -n "${ARCH_OPENSSL_INCLUDE_DIR}" ]]; then \
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_LIB_DIR=${ARCH_OPENSSL_LIB_DIR}" >> /env-cargo && \
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_INCLUDE_DIR=${ARCH_OPENSSL_INCLUDE_DIR}" >> /env-cargo ; \
fi && \
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
# Because of this we generate the needed environment variables here which we can load in the needed steps.
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
echo "export PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /env-cargo && \
echo "export CROSS_COMPILE=1" >> /env-cargo && \
echo "export OPENSSL_INCLUDE_DIR=/usr/include/$(xx-info)" >> /env-cargo && \
echo "export OPENSSL_LIB_DIR=/usr/lib/$(xx-info)" >> /env-cargo ; \
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
# For some architectures `xx-info` returns a triple which doesn't matches the path on disk
# In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg
if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \
echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \
else \
echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \
fi && \
echo "# End of env-cargo" >> /env-cargo ; \
fi && \
# Output the current contents of the file
cat /env-cargo

View File

@@ -109,24 +109,24 @@ WORKDIR /app
{% if base == "debian" %}
# Environment variables for Cargo on Debian based builds
ARG ARCH_OPENSSL_LIB_DIR \
ARCH_OPENSSL_INCLUDE_DIR
ARG TARGET_PKG_CONFIG_PATH
RUN source /env-cargo && \
if xx-info is-cross ; then \
# Some special variables if needed to override some build paths
if [[ -n "${ARCH_OPENSSL_LIB_DIR}" && -n "${ARCH_OPENSSL_INCLUDE_DIR}" ]]; then \
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_LIB_DIR=${ARCH_OPENSSL_LIB_DIR}" >> /env-cargo && \
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_INCLUDE_DIR=${ARCH_OPENSSL_INCLUDE_DIR}" >> /env-cargo ; \
fi && \
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
# Because of this we generate the needed environment variables here which we can load in the needed steps.
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
echo "export PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /env-cargo && \
echo "export CROSS_COMPILE=1" >> /env-cargo && \
echo "export OPENSSL_INCLUDE_DIR=/usr/include/$(xx-info)" >> /env-cargo && \
echo "export OPENSSL_LIB_DIR=/usr/lib/$(xx-info)" >> /env-cargo ; \
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
# For some architectures `xx-info` returns a triple which doesn't matches the path on disk
# In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg
if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \
echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \
else \
echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \
fi && \
echo "# End of env-cargo" >> /env-cargo ; \
fi && \
# Output the current contents of the file
cat /env-cargo

View File

@@ -133,8 +133,7 @@ target "debian-386" {
platforms = ["linux/386"]
tags = generate_tags("", "-386")
args = {
ARCH_OPENSSL_LIB_DIR = "/usr/lib/i386-linux-gnu"
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/i386-linux-gnu"
TARGET_PKG_CONFIG_PATH = "/usr/lib/i386-linux-gnu/pkgconfig"
}
}
@@ -142,20 +141,12 @@ target "debian-ppc64le" {
inherits = ["debian"]
platforms = ["linux/ppc64le"]
tags = generate_tags("", "-ppc64le")
args = {
ARCH_OPENSSL_LIB_DIR = "/usr/lib/powerpc64le-linux-gnu"
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/powerpc64le-linux-gnu"
}
}
target "debian-s390x" {
inherits = ["debian"]
platforms = ["linux/s390x"]
tags = generate_tags("", "-s390x")
args = {
ARCH_OPENSSL_LIB_DIR = "/usr/lib/s390x-linux-gnu"
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/s390x-linux-gnu"
}
}
// ==== End of unsupported Debian architecture targets ===

View File

@@ -9,5 +9,8 @@ path = "src/lib.rs"
proc-macro = true
[dependencies]
quote = "1.0.38"
syn = "2.0.96"
quote = "1.0.40"
syn = "2.0.101"
[lints]
workspace = true

View File

@@ -1,5 +1,3 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
@@ -12,7 +10,7 @@ pub fn derive_uuid_from_param(input: TokenStream) -> TokenStream {
fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
let gen_derive = quote! {
#[automatically_derived]
impl<'r> rocket::request::FromParam<'r> for #name {
type Error = ();
@@ -27,7 +25,7 @@ fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream {
}
}
};
gen.into()
gen_derive.into()
}
#[proc_macro_derive(IdFromParam)]
@@ -39,7 +37,7 @@ pub fn derive_id_from_param(input: TokenStream) -> TokenStream {
fn impl_derive_safestring_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
let gen_derive = quote! {
#[automatically_derived]
impl<'r> rocket::request::FromParam<'r> for #name {
type Error = ();
@@ -54,5 +52,5 @@ fn impl_derive_safestring_macro(ast: &syn::DeriveInput) -> TokenStream {
}
}
};
gen.into()
gen_derive.into()
}

View File

@@ -1,4 +1,4 @@
[toolchain]
channel = "1.84.1"
channel = "1.87.0"
components = [ "rustfmt", "clippy" ]
profile = "minimal"

View File

@@ -102,7 +102,7 @@ const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000";
pub const FAKE_ADMIN_UUID: &str = "00000000-0000-0000-0000-000000000000";
fn admin_path() -> String {
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
format!("{}{ADMIN_PATH}", CONFIG.domain_path())
}
#[derive(Debug)]
@@ -206,7 +206,7 @@ fn post_admin_login(
cookies.add(cookie);
if let Some(redirect) = redirect {
Ok(Redirect::to(format!("{}{}", admin_path(), redirect)))
Ok(Redirect::to(format!("{}{redirect}", admin_path())))
} else {
Err(AdminResponse::Ok(render_admin_page()))
}
@@ -421,13 +421,13 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em
async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
nt.send_logout(&user, None).await;
nt.send_logout(&user, None, &mut conn).await;
if CONFIG.push_enabled() {
for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await {
match unregister_push_device(device.push_uuid).await {
match unregister_push_device(&device.push_uuid).await {
Ok(r) => r,
Err(e) => error!("Unable to unregister devices from Bitwarden server: {}", e),
Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"),
};
}
}
@@ -447,7 +447,7 @@ async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt:
let save_result = user.save(&mut conn).await;
nt.send_logout(&user, None).await;
nt.send_logout(&user, None, &mut conn).await;
save_result
}
@@ -591,20 +591,14 @@ struct GitCommit {
sha: String,
}
#[derive(Deserialize)]
struct TimeApi {
year: u16,
month: u8,
day: u8,
hour: u8,
minute: u8,
seconds: u8,
}
async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.json::<T>().await?)
}
async fn get_text_api(url: &str) -> Result<String, Error> {
Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.text().await?)
}
async fn has_http_access() -> bool {
let Ok(req) = make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") else {
return false;
@@ -616,9 +610,10 @@ async fn has_http_access() -> bool {
}
use cached::proc_macro::cached;
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already.
/// It will cache this function for 300 seconds (5 minutes) which should prevent the exhaustion of the rate limit.
#[cached(time = 300, sync_writes = true)]
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already
/// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit
/// Any cache will be lost if Vaultwarden is restarted
#[cached(time = 600, sync_writes = "default")]
async fn get_release_info(has_http_access: bool, running_within_container: bool) -> (String, String, String) {
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
if has_http_access {
@@ -636,7 +631,7 @@ async fn get_release_info(has_http_access: bool, running_within_container: bool)
}
_ => "-".to_string(),
},
// Do not fetch the web-vault version when running within a container.
// Do not fetch the web-vault version when running within a container
// The web-vault version is embedded within the container it self, and should not be updated manually
if running_within_container {
"-".to_string()
@@ -658,17 +653,18 @@ async fn get_release_info(has_http_access: bool, running_within_container: bool)
async fn get_ntp_time(has_http_access: bool) -> String {
if has_http_access {
if let Ok(ntp_time) = get_json_api::<TimeApi>("https://www.timeapi.io/api/Time/current/zone?timeZone=UTC").await
{
return format!(
"{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{seconds:02} UTC",
year = ntp_time.year,
month = ntp_time.month,
day = ntp_time.day,
hour = ntp_time.hour,
minute = ntp_time.minute,
seconds = ntp_time.seconds
);
if let Ok(cf_trace) = get_text_api("https://cloudflare.com/cdn-cgi/trace").await {
for line in cf_trace.lines() {
if let Some((key, value)) = line.split_once('=') {
if key == "ts" {
let ts = value.split_once('.').map_or(value, |(s, _)| s);
if let Ok(dt) = chrono::DateTime::parse_from_str(ts, "%s") {
return dt.format("%Y-%m-%d %H:%M:%S UTC").to_string();
}
break;
}
}
}
}
}
String::from("Unable to fetch NTP time.")
@@ -701,6 +697,12 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
// Get current running versions
let web_vault_version = get_web_vault_version();
// Check if the running version is newer than the latest stable released version
let web_ver_match = semver::VersionReq::parse(&format!(">{latest_web_build}")).unwrap();
let web_vault_pre_release = web_ver_match.matches(
&semver::Version::parse(&web_vault_version).unwrap_or_else(|_| semver::Version::parse("2025.1.1").unwrap()),
);
let diagnostics_json = json!({
"dns_resolved": dns_resolved,
"current_release": VERSION,
@@ -709,6 +711,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
"web_vault_enabled": &CONFIG.web_vault_enabled(),
"web_vault_version": web_vault_version,
"latest_web_build": latest_web_build,
"web_vault_pre_release": web_vault_pre_release,
"running_within_container": running_within_container,
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
"has_http_access": has_http_access,
@@ -724,6 +727,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
"overrides": &CONFIG.get_overrides().join(", "),
"host_arch": env::consts::ARCH,
"host_os": env::consts::OS,
"tz_env": env::var("TZ").unwrap_or_default(),
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference
"ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference

View File

@@ -70,18 +70,31 @@ pub fn routes() -> Vec<rocket::Route> {
#[serde(rename_all = "camelCase")]
pub struct RegisterData {
email: String,
kdf: Option<i32>,
kdf_iterations: Option<i32>,
kdf_memory: Option<i32>,
kdf_parallelism: Option<i32>,
#[serde(alias = "userSymmetricKey")]
key: String,
#[serde(alias = "userAsymmetricKeys")]
keys: Option<KeysData>,
master_password_hash: String,
master_password_hint: Option<String>,
name: Option<String>,
token: Option<String>,
#[allow(dead_code)]
organization_user_id: Option<MembershipId>,
// Used only from the register/finish endpoint
email_verification_token: Option<String>,
accept_emergency_access_id: Option<EmergencyAccessId>,
accept_emergency_access_invite_token: Option<String>,
#[serde(alias = "token")]
org_invite_token: Option<String>,
}
#[derive(Debug, Deserialize)]
@@ -115,22 +128,86 @@ async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &mut DbCon
if CONFIG.email_2fa_enforce_on_verified_invite() {
return true;
}
if member_id.is_some() {
return OrgPolicy::is_enabled_for_member(&member_id.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
.await;
if let Some(member_id) = member_id {
return OrgPolicy::is_enabled_for_member(&member_id, OrgPolicyType::TwoFactorAuthentication, conn).await;
}
false
}
#[post("/accounts/register", data = "<data>")]
async fn register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
_register(data, conn).await
_register(data, false, conn).await
}
pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult {
let data: RegisterData = data.into_inner();
pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut conn: DbConn) -> JsonResult {
let mut data: RegisterData = data.into_inner();
let email = data.email.to_lowercase();
let mut email_verified = false;
let mut pending_emergency_access = None;
// First, validate the provided verification tokens
if email_verification {
match (
&data.email_verification_token,
&data.accept_emergency_access_id,
&data.accept_emergency_access_invite_token,
&data.organization_user_id,
&data.org_invite_token,
) {
// Normal user registration, when email verification is required
(Some(email_verification_token), None, None, None, None) => {
let claims = crate::auth::decode_register_verify(email_verification_token)?;
if claims.sub != data.email {
err!("Email verification token does not match email");
}
// During this call we don't get the name, so extract it from the claims
if claims.name.is_some() {
data.name = claims.name;
}
email_verified = claims.verified;
}
// Emergency access registration
(None, Some(accept_emergency_access_id), Some(accept_emergency_access_invite_token), None, None) => {
if !CONFIG.emergency_access_allowed() {
err!("Emergency access is not enabled.")
}
let claims = crate::auth::decode_emergency_access_invite(accept_emergency_access_invite_token)?;
if claims.email != data.email {
err!("Claim email does not match email")
}
if &claims.emer_id != accept_emergency_access_id {
err!("Claim emer_id does not match accept_emergency_access_id")
}
pending_emergency_access = Some((accept_emergency_access_id, claims));
email_verified = true;
}
// Org invite
(None, None, None, Some(organization_user_id), Some(org_invite_token)) => {
let claims = decode_invite(org_invite_token)?;
if claims.email != data.email {
err!("Claim email does not match email")
}
if &claims.member_id != organization_user_id {
err!("Claim org_user_id does not match organization_user_id")
}
email_verified = true;
}
_ => {
err!("Registration is missing required parameters")
}
}
}
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
// This also prevents issues with very long usernames causing to large JWT's. See #2419
if let Some(ref name) = data.name {
@@ -144,20 +221,17 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
let password_hint = clean_password_hint(&data.master_password_hint);
enforce_password_hint_setting(&password_hint)?;
let mut verified_by_invite = false;
let mut user = match User::find_by_mail(&email, &mut conn).await {
Some(mut user) => {
Some(user) => {
if !user.password_hash.is_empty() {
err!("Registration not allowed or user already exists")
}
if let Some(token) = data.token {
if let Some(token) = data.org_invite_token {
let claims = decode_invite(&token)?;
if claims.email == email {
// Verify the email address when signing up via a valid invite token
verified_by_invite = true;
user.verified_at = Some(Utc::now().naive_utc());
email_verified = true;
user
} else {
err!("Registration email does not match invite email")
@@ -181,7 +255,10 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
// Order is important here; the invitation check must come first
// because the vaultwarden admin can invite anyone, regardless
// of other signup restrictions.
if Invitation::take(&email, &mut conn).await || CONFIG.is_signup_allowed(&email) {
if Invitation::take(&email, &mut conn).await
|| CONFIG.is_signup_allowed(&email)
|| pending_emergency_access.is_some()
{
User::new(email.clone())
} else {
err!("Registration not allowed or user already exists")
@@ -216,17 +293,21 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
user.public_key = Some(keys.public_key);
}
if email_verified {
user.verified_at = Some(Utc::now().naive_utc());
}
if CONFIG.mail_enabled() {
if CONFIG.signups_verify() && !verified_by_invite {
if CONFIG.signups_verify() && !email_verified {
if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid).await {
error!("Error sending welcome email: {:#?}", e);
error!("Error sending welcome email: {e:#?}");
}
user.last_verifying_at = Some(user.created_at);
} else if let Err(e) = mail::send_welcome(&user.email).await {
error!("Error sending welcome email: {:#?}", e);
error!("Error sending welcome email: {e:#?}");
}
if verified_by_invite && is_email_2fa_required(data.organization_user_id, &mut conn).await {
if email_verified && is_email_2fa_required(data.organization_user_id, &mut conn).await {
email::activate_email_2fa(&user, &mut conn).await.ok();
}
}
@@ -255,7 +336,6 @@ async fn profile(headers: Headers, mut conn: DbConn) -> Json<Value> {
#[serde(rename_all = "camelCase")]
struct ProfileData {
// culture: String, // Ignored, always use en-US
// masterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData
name: String,
}
@@ -381,7 +461,7 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, mut conn: D
// Prevent logging out the client where the user requested this endpoint from.
// If you do logout the user it will causes issues at the client side.
// Adding the device uuid will prevent this.
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;
save_result
}
@@ -441,7 +521,7 @@ async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, mut conn: DbConn,
user.set_password(&data.new_master_password_hash, Some(data.key), true, None);
let save_result = user.save(&mut conn).await;
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;
save_result
}
@@ -653,7 +733,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Prevent logging out the client where the user requested this endpoint from.
// If you do logout the user it will causes issues at the client side.
// Adding the device uuid will prevent this.
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;
save_result
}
@@ -669,7 +749,7 @@ async fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
user.reset_security_stamp();
let save_result = user.save(&mut conn).await;
nt.send_logout(&user, None).await;
nt.send_logout(&user, None, &mut conn).await;
save_result
}
@@ -695,6 +775,11 @@ async fn post_email_token(data: Json<EmailTokenData>, headers: Headers, mut conn
}
if User::find_by_mail(&data.new_email, &mut conn).await.is_some() {
if CONFIG.mail_enabled() {
if let Err(e) = mail::send_change_email_existing(&data.new_email, &user.email).await {
error!("Error sending change-email-existing email: {e:#?}");
}
}
err!("Email already in use");
}
@@ -706,10 +791,10 @@ async fn post_email_token(data: Json<EmailTokenData>, headers: Headers, mut conn
if CONFIG.mail_enabled() {
if let Err(e) = mail::send_change_email(&data.new_email, &token).await {
error!("Error sending change-email email: {:#?}", e);
error!("Error sending change-email email: {e:#?}");
}
} else {
debug!("Email change request for user ({}) to email ({}) with token ({})", user.uuid, data.new_email, token);
debug!("Email change request for user ({}) to email ({}) with token ({token})", user.uuid, data.new_email);
}
user.email_new = Some(data.new_email);
@@ -777,7 +862,7 @@ async fn post_email(data: Json<ChangeEmailData>, headers: Headers, mut conn: DbC
let save_result = user.save(&mut conn).await;
nt.send_logout(&user, None).await;
nt.send_logout(&user, None, &mut conn).await;
save_result
}
@@ -791,7 +876,7 @@ async fn post_verify_email(headers: Headers) -> EmptyResult {
}
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {
error!("Error sending verify_email email: {:#?}", e);
error!("Error sending verify_email email: {e:#?}");
}
Ok(())
@@ -822,7 +907,7 @@ async fn post_verify_email_token(data: Json<VerifyEmailTokenData>, mut conn: DbC
user.last_verifying_at = None;
user.login_verify_count = 0;
if let Err(e) = user.save(&mut conn).await {
error!("Error saving email verification: {:#?}", e);
error!("Error saving email verification: {e:#?}");
}
Ok(())
@@ -841,7 +926,7 @@ async fn post_delete_recover(data: Json<DeleteRecoverData>, mut conn: DbConn) ->
if CONFIG.mail_enabled() {
if let Some(user) = User::find_by_mail(&data.email, &mut conn).await {
if let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await {
error!("Error sending delete account email: {:#?}", e);
error!("Error sending delete account email: {e:#?}");
}
}
Ok(())
@@ -975,7 +1060,7 @@ pub async fn _prelogin(data: Json<PreloginData>, mut conn: DbConn) -> Json<Value
}))
}
// https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SecretVerificationRequest {
@@ -1116,19 +1201,14 @@ async fn put_device_token(
err!(format!("Error: device {device_id} should be present before a token can be assigned"))
};
// if the device already has been registered
if device.is_registered() {
// check if the new token is the same as the registered token
if device.push_token.is_some() && device.push_token.unwrap() == token.clone() {
debug!("Device {} is already registered and token is the same", device_id);
return Ok(());
} else {
// Try to unregister already registered device
unregister_push_device(device.push_uuid).await.ok();
}
// clear the push_uuid
device.push_uuid = None;
// Check if the new token is the same as the registered token
// Although upstream seems to always register a device on login, we do not.
// Unless this causes issues, lets keep it this way, else we might need to also register on every login.
if device.push_token.as_ref() == Some(&token) {
debug!("Device {device_id} for user {} is already registered and token is identical", headers.user.uuid);
return Ok(());
}
device.push_token = Some(token);
if let Err(e) = device.save(&mut conn).await {
err!(format!("An error occurred while trying to save the device push token: {e}"));
@@ -1142,16 +1222,19 @@ async fn put_device_token(
#[put("/devices/identifier/<device_id>/clear-token")]
async fn put_clear_device_token(device_id: DeviceId, mut conn: DbConn) -> EmptyResult {
// This only clears push token
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Controllers/DevicesController.cs#L215
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Services/Implementations/DeviceService.cs#L37
// This is somehow not implemented in any app, added it in case it is required
// 2025: Also, it looks like it only clears the first found device upstream, which is probably faulty.
// This because currently multiple accounts could be on the same device/app and that would cause issues.
// Vaultwarden removes the push-token for all devices, but this probably means we should also unregister all these devices.
if !CONFIG.push_enabled() {
return Ok(());
}
if let Some(device) = Device::find_by_uuid(&device_id, &mut conn).await {
Device::clear_push_token_by_uuid(&device_id, &mut conn).await?;
unregister_push_device(device.push_uuid).await?;
unregister_push_device(&device.push_uuid).await?;
}
Ok(())
@@ -1189,10 +1272,10 @@ async fn post_auth_request(
};
// Validate device uuid and type
match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await {
Some(device) if device.atype == client_headers.device_type => {}
let device = match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await {
Some(device) if device.atype == client_headers.device_type => device,
_ => err!("AuthRequest doesn't exist", "Device verification failed"),
}
};
let mut auth_request = AuthRequest::new(
user.uuid.clone(),
@@ -1204,7 +1287,7 @@ async fn post_auth_request(
);
auth_request.save(&mut conn).await?;
nt.send_auth_request(&user.uuid, &auth_request.uuid, &data.device_identifier, &mut conn).await;
nt.send_auth_request(&user.uuid, &auth_request.uuid, &device, &mut conn).await;
log_user_event(
EventType::UserRequestedDeviceApproval as i32,
@@ -1279,6 +1362,10 @@ async fn put_auth_request(
err!("AuthRequest doesn't exist", "Record not found or user uuid does not match")
};
if headers.device.uuid != data.device_identifier {
err!("AuthRequest doesn't exist", "Device verification failed")
}
if auth_request.approved.is_some() {
err!("An authentication request with the same device already exists")
}
@@ -1295,7 +1382,7 @@ async fn put_auth_request(
auth_request.save(&mut conn).await?;
ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &data.device_identifier, &mut conn).await;
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &mut conn).await;
log_user_event(
EventType::OrganizationUserApprovedAuthRequest as i32,

View File

@@ -381,7 +381,7 @@ pub async fn update_cipher_from_data(
if let Some(dt) = data.last_known_revision_date {
match NaiveDateTime::parse_from_str(&dt, "%+") {
// ISO 8601 format
Err(err) => warn!("Error parsing LastKnownRevisionDate '{}': {}", dt, err),
Err(err) => warn!("Error parsing LastKnownRevisionDate '{dt}': {err}"),
Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => {
err!("The client copy of this cipher is out of date. Resync the client and try again.")
}
@@ -535,7 +535,7 @@ pub async fn update_cipher_from_data(
ut,
cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
&headers.device,
shared_to_collections,
conn,
)
@@ -612,7 +612,7 @@ async fn post_ciphers_import(
let mut user = headers.user;
user.update_revision(&mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user).await;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await;
Ok(())
}
@@ -808,7 +808,7 @@ async fn post_collections_update(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(&mut conn).await,
&headers.device.uuid,
&headers.device,
Some(Vec::from_iter(posted_collections)),
&mut conn,
)
@@ -885,7 +885,7 @@ async fn post_collections_admin(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(&mut conn).await,
&headers.device.uuid,
&headers.device,
Some(Vec::from_iter(posted_collections)),
&mut conn,
)
@@ -1105,7 +1105,7 @@ async fn post_attachment_v2(
Attachment::new(attachment_id.clone(), cipher.uuid.clone(), data.file_name, file_size, Some(data.key));
attachment.save(&mut conn).await.expect("Error saving attachment");
let url = format!("/ciphers/{}/attachment/{}", cipher.uuid, attachment_id);
let url = format!("/ciphers/{}/attachment/{attachment_id}", cipher.uuid);
let response_key = match data.admin_request {
Some(b) if b => "cipherMiniResponse",
_ => "cipherResponse",
@@ -1281,7 +1281,7 @@ async fn save_attachment(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(&mut conn).await,
&headers.device.uuid,
&headers.device,
None,
&mut conn,
)
@@ -1376,7 +1376,7 @@ async fn delete_attachment_post_admin(
headers: Headers,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
) -> JsonResult {
delete_attachment(cipher_id, attachment_id, headers, conn, nt).await
}
@@ -1387,7 +1387,7 @@ async fn delete_attachment_post(
headers: Headers,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
) -> JsonResult {
delete_attachment(cipher_id, attachment_id, headers, conn, nt).await
}
@@ -1398,7 +1398,7 @@ async fn delete_attachment(
headers: Headers,
mut conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
) -> JsonResult {
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &mut conn, &nt).await
}
@@ -1409,7 +1409,7 @@ async fn delete_attachment_admin(
headers: Headers,
mut conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
) -> JsonResult {
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &mut conn, &nt).await
}
@@ -1581,8 +1581,8 @@ async fn move_cipher_selected(
nt.send_cipher_update(
UpdateType::SyncCipherUpdate,
&cipher,
&[user_id.clone()],
&headers.device.uuid,
std::slice::from_ref(&user_id),
&headers.device,
None,
&mut conn,
)
@@ -1629,7 +1629,7 @@ async fn delete_all(
Some(member) => {
if member.atype == MembershipType::Owner {
Cipher::delete_all_by_organization(&org_data.org_id, &mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user).await;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await;
log_event(
EventType::OrganizationPurgedVault as i32,
@@ -1662,7 +1662,7 @@ async fn delete_all(
}
user.update_revision(&mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user).await;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await;
Ok(())
}
@@ -1691,7 +1691,7 @@ async fn _delete_cipher_by_uuid(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
&headers.device,
None,
conn,
)
@@ -1702,7 +1702,7 @@ async fn _delete_cipher_by_uuid(
UpdateType::SyncCipherDelete,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
&headers.device,
None,
conn,
)
@@ -1767,7 +1767,7 @@ async fn _restore_cipher_by_uuid(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
&headers.device,
None,
conn,
)
@@ -1818,7 +1818,7 @@ async fn _delete_cipher_attachment_by_id(
headers: &Headers,
conn: &mut DbConn,
nt: &Notify<'_>,
) -> EmptyResult {
) -> JsonResult {
let Some(attachment) = Attachment::find_by_id(attachment_id, conn).await else {
err!("Attachment doesn't exist")
};
@@ -1841,17 +1841,17 @@ async fn _delete_cipher_attachment_by_id(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
&headers.device,
None,
conn,
)
.await;
if let Some(org_id) = cipher.organization_uuid {
if let Some(ref org_id) = cipher.organization_uuid {
log_event(
EventType::CipherAttachmentDeleted as i32,
&cipher.uuid,
&org_id,
org_id,
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
@@ -1859,7 +1859,8 @@ async fn _delete_cipher_attachment_by_id(
)
.await;
}
Ok(())
let cipher_json = cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await;
Ok(Json(json!({"cipher":cipher_json})))
}
/// This will hold all the necessary data to improve a full sync of all the ciphers

View File

@@ -227,7 +227,7 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
let (grantee_user, new_user) = match User::find_by_mail(&email, &mut conn).await {
None => {
if !CONFIG.invitations_allowed() {
err!(format!("Grantee user does not exist: {}", &email))
err!(format!("Grantee user does not exist: {email}"))
}
if !CONFIG.is_email_domain_allowed(&email) {

View File

@@ -29,7 +29,7 @@ struct EventRange {
continuation_token: Option<String>,
}
// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/EventsController.cs#L87
#[get("/organizations/<org_id>/events?<data..>")]
async fn get_org_events(
org_id: OrganizationId,
@@ -169,8 +169,8 @@ struct EventCollection {
}
// Upstream:
// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Events/Controllers/CollectController.cs
// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Events/Controllers/CollectController.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
#[post("/collect", format = "application/json", data = "<data>")]
async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers, mut conn: DbConn) -> EmptyResult {
if !CONFIG.org_events_enabled() {

View File

@@ -45,7 +45,7 @@ async fn post_folders(data: Json<FolderData>, headers: Headers, mut conn: DbConn
let mut folder = Folder::new(headers.user.uuid, data.name);
folder.save(&mut conn).await?;
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid, &mut conn).await;
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device, &mut conn).await;
Ok(Json(folder.to_json()))
}
@@ -78,7 +78,7 @@ async fn put_folder(
folder.name = data.name;
folder.save(&mut conn).await?;
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid, &mut conn).await;
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device, &mut conn).await;
Ok(Json(folder.to_json()))
}
@@ -97,6 +97,6 @@ async fn delete_folder(folder_id: FolderId, headers: Headers, mut conn: DbConn,
// Delete the actual folder entry
folder.delete(&mut conn).await?;
nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid, &mut conn).await;
nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device, &mut conn).await;
Ok(())
}

View File

@@ -124,7 +124,7 @@ async fn post_eq_domains(
user.save(&mut conn).await?;
nt.send_user_update(UpdateType::SyncSettings, &user).await;
nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &mut conn).await;
Ok(Json(json!({})))
}
@@ -199,11 +199,16 @@ fn get_api_webauthn(_headers: Headers) -> Json<Value> {
#[get("/config")]
fn config() -> Json<Value> {
let domain = crate::CONFIG.domain();
// Official available feature flags can be found here:
// Server (v2025.5.0): https://github.com/bitwarden/server/blob/4a7db112a0952c6df8bacf36c317e9c4e58c3651/src/Core/Constants.cs#L102
// Client (v2025.5.0): https://github.com/bitwarden/clients/blob/9df8a3cc50ed45f52513e62c23fcc8a4b745f078/libs/common/src/enums/feature-flag.enum.ts#L10
// Android (v2025.4.0): https://github.com/bitwarden/android/blob/bee09de972c3870de0d54a0067996be473ec55c7/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L27
// iOS (v2025.4.0): https://github.com/bitwarden/ios/blob/956e05db67344c912e3a1b8cb2609165d67da1c9/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
let mut feature_states =
parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
// Force the new key rotation feature
feature_states.insert("key-rotation-improvements".to_string(), true);
feature_states.insert("flexible-collections-v-1".to_string(), false);
feature_states.insert("duo-redirect".to_string(), true);
feature_states.insert("email-verification".to_string(), true);
feature_states.insert("unauth-ui-refresh".to_string(), true);
Json(json!({
// Note: The clients use this version to handle backwards compatibility concerns
@@ -211,7 +216,7 @@ fn config() -> Json<Value> {
// We should make sure that we keep this updated when we support the new server features
// Version history:
// - Individual cipher key encryption: 2024.2.0
"version": "2025.1.0",
"version": "2025.4.0",
"gitHash": option_env!("GIT_REV"),
"server": {
"name": "Vaultwarden",
@@ -226,6 +231,12 @@ fn config() -> Json<Value> {
"identity": format!("{domain}/identity"),
"notifications": format!("{domain}/notifications"),
"sso": "",
"cloudRegion": null,
},
// Bitwarden uses this for the self-hosted servers to indicate the default push technology
"push": {
"pushTechnology": 0,
"vapidPublicKey": null
},
"featureStates": feature_states,
"object": "config",

View File

@@ -10,10 +10,7 @@ use crate::{
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
},
auth::{
decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders,
OwnerHeaders,
},
auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders},
db::{models::*, DbConn},
mail,
util::{convert_json_key_lcase_first, NumberOrString},
@@ -38,6 +35,7 @@ pub fn routes() -> Vec<Route> {
post_organization_collections,
delete_organization_collection_member,
post_organization_collection_delete_member,
post_bulk_access_collections,
post_organization_collection_update,
put_organization_collection_update,
delete_organization_collection,
@@ -129,17 +127,17 @@ struct OrganizationUpdateData {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct NewCollectionData {
struct FullCollectionData {
name: String,
groups: Vec<NewCollectionGroupData>,
users: Vec<NewCollectionMemberData>,
groups: Vec<CollectionGroupData>,
users: Vec<CollectionMembershipData>,
id: Option<CollectionId>,
external_id: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct NewCollectionGroupData {
struct CollectionGroupData {
hide_passwords: bool,
id: GroupId,
read_only: bool,
@@ -148,7 +146,7 @@ struct NewCollectionGroupData {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct NewCollectionMemberData {
struct CollectionMembershipData {
hide_passwords: bool,
id: MembershipId,
read_only: bool,
@@ -376,6 +374,21 @@ async fn get_org_collections_details(
|| (CONFIG.org_groups_enabled()
&& GroupUser::has_full_access_by_member(&org_id, &member.uuid, &mut conn).await);
// Get all admins, owners and managers who can manage/access all
// Those are currently not listed in the col_users but need to be listed too.
let manage_all_members: Vec<Value> = Membership::find_confirmed_and_manage_all_by_org(&org_id, &mut conn)
.await
.into_iter()
.map(|member| {
json!({
"id": member.uuid,
"readOnly": false,
"hidePasswords": false,
"manage": true,
})
})
.collect();
for col in Collection::find_by_organization(&org_id, &mut conn).await {
// check whether the current user has access to the given collection
let assigned = has_full_access_to_org
@@ -384,7 +397,7 @@ async fn get_org_collections_details(
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await);
// get the users assigned directly to the given collection
let users: Vec<Value> = col_users
let mut users: Vec<Value> = col_users
.iter()
.filter(|collection_member| collection_member.collection_uuid == col.uuid)
.map(|collection_member| {
@@ -393,6 +406,7 @@ async fn get_org_collections_details(
)
})
.collect();
users.extend_from_slice(&manage_all_members);
// get the group details for the given collection
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
@@ -429,13 +443,13 @@ async fn _get_org_collections(org_id: &OrganizationId, conn: &mut DbConn) -> Val
async fn post_organization_collections(
org_id: OrganizationId,
headers: ManagerHeadersLoose,
data: Json<NewCollectionData>,
data: Json<FullCollectionData>,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let data: NewCollectionData = data.into_inner();
let data: FullCollectionData = data.into_inner();
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
err!("Can't find organization details")
@@ -488,29 +502,104 @@ async fn post_organization_collections(
Ok(Json(collection.to_json_details(&headers.membership.user_uuid, None, &mut conn).await))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct BulkCollectionAccessData {
collection_ids: Vec<CollectionId>,
groups: Vec<CollectionGroupData>,
users: Vec<CollectionMembershipData>,
}
#[post("/organizations/<org_id>/collections/bulk-access", data = "<data>", rank = 1)]
async fn post_bulk_access_collections(
org_id: OrganizationId,
headers: ManagerHeadersLoose,
data: Json<BulkCollectionAccessData>,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let data: BulkCollectionAccessData = data.into_inner();
if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() {
err!("Can't find organization details")
};
for col_id in data.collection_ids {
let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else {
err!("Collection not found")
};
// update collection modification date
collection.save(&mut conn).await?;
log_event(
EventType::CollectionUpdated as i32,
&collection.uuid,
&org_id,
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
)
.await;
CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?;
for group in &data.groups {
CollectionGroup::new(col_id.clone(), group.id.clone(), group.read_only, group.hide_passwords, group.manage)
.save(&mut conn)
.await?;
}
CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?;
for user in &data.users {
let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &mut conn).await else {
err!("User is not part of organization")
};
if member.access_all {
continue;
}
CollectionUser::save(
&member.user_uuid,
&col_id,
user.read_only,
user.hide_passwords,
user.manage,
&mut conn,
)
.await?;
}
}
Ok(())
}
#[put("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
async fn put_organization_collection_update(
org_id: OrganizationId,
col_id: CollectionId,
headers: ManagerHeaders,
data: Json<NewCollectionData>,
data: Json<FullCollectionData>,
conn: DbConn,
) -> JsonResult {
post_organization_collection_update(org_id, col_id, headers, data, conn).await
}
#[post("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
#[post("/organizations/<org_id>/collections/<col_id>", data = "<data>", rank = 2)]
async fn post_organization_collection_update(
org_id: OrganizationId,
col_id: CollectionId,
headers: ManagerHeaders,
data: Json<NewCollectionData>,
data: Json<FullCollectionData>,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: NewCollectionData = data.into_inner();
let data: FullCollectionData = data.into_inner();
if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() {
err!("Can't find organization details")
@@ -608,6 +697,9 @@ async fn _delete_organization_collection(
headers: &ManagerHeaders,
conn: &mut DbConn,
) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, conn).await else {
err!("Collection not found", "Collection does not exist or does not belong to this organization")
};
@@ -781,7 +873,7 @@ async fn get_collection_users(
async fn put_collection_users(
org_id: OrganizationId,
col_id: CollectionId,
data: Json<Vec<MembershipData>>,
data: Json<Vec<CollectionMembershipData>>,
headers: ManagerHeaders,
mut conn: DbConn,
) -> EmptyResult {
@@ -820,7 +912,7 @@ struct OrgIdData {
#[get("/ciphers/organization-details?<data..>")]
async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: DbConn) -> JsonResult {
if data.organization_id != headers.org_id {
if data.organization_id != headers.membership.org_uuid {
err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code);
}
@@ -913,24 +1005,6 @@ async fn post_org_keys(
})))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CollectionData {
id: CollectionId,
read_only: bool,
hide_passwords: bool,
manage: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct MembershipData {
id: MembershipId,
read_only: bool,
hide_passwords: bool,
manage: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct InviteData {
@@ -939,8 +1013,6 @@ struct InviteData {
r#type: NumberOrString,
collections: Option<Vec<CollectionData>>,
#[serde(default)]
access_all: bool,
#[serde(default)]
permissions: HashMap<String, Value>,
}
@@ -954,7 +1026,7 @@ async fn send_invite(
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let mut data: InviteData = data.into_inner();
let data: InviteData = data.into_inner();
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
// The from_str() will convert the custom role type into a manager role type
@@ -972,13 +1044,11 @@ async fn send_invite(
// 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 access_all = new_type >= MembershipType::Admin
|| (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)));
let mut user_created: bool = false;
for email in data.emails.iter() {
@@ -1016,7 +1086,6 @@ async fn send_invite(
};
let mut new_member = Membership::new(user.uuid.clone(), org_id.clone());
let access_all = data.access_all;
new_member.access_all = access_all;
new_member.atype = new_type;
new_member.status = member_status;
@@ -1130,6 +1199,9 @@ async fn reinvite_member(
headers: AdminHeaders,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
_reinvite_member(&org_id, &member_id, &headers.user.email, &mut conn).await
}
@@ -1347,6 +1419,9 @@ async fn _confirm_invite(
conn: &mut DbConn,
nt: &Notify<'_>,
) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if key.is_empty() || member_id.is_empty() {
err!("Key or UserId is not set, unable to process request");
}
@@ -1410,7 +1485,7 @@ async fn _confirm_invite(
let save_result = member_to_confirm.save(conn).await;
if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {
nt.send_user_update(UpdateType::SyncOrgKeys, &user).await;
nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await;
}
save_result
@@ -1467,8 +1542,6 @@ struct EditUserData {
collections: Option<Vec<CollectionData>>,
groups: Option<Vec<GroupId>>,
#[serde(default)]
access_all: bool,
#[serde(default)]
permissions: HashMap<String, Value>,
}
@@ -1494,7 +1567,7 @@ async fn edit_member(
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let mut data: EditUserData = data.into_inner();
let data: EditUserData = data.into_inner();
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
// The from_str() will convert the custom role type into a manager role type
@@ -1507,13 +1580,11 @@ async fn edit_member(
// 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 access_all = new_type >= MembershipType::Admin
|| (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)));
let mut member_to_edit = match Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await {
Some(member) => member,
@@ -1559,7 +1630,7 @@ async fn edit_member(
}
}
member_to_edit.access_all = data.access_all;
member_to_edit.access_all = access_all;
member_to_edit.atype = new_type as i32;
// Delete all the odd collections
@@ -1568,7 +1639,7 @@ async fn edit_member(
}
// If no accessAll, add the collections received
if !data.access_all {
if !access_all {
for col in data.collections.iter().flatten() {
match Collection::find_by_uuid_and_org(&col.id, &org_id, &mut conn).await {
None => err!("Collection not found in Organization"),
@@ -1673,6 +1744,9 @@ async fn _delete_member(
conn: &mut DbConn,
nt: &Notify<'_>,
) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(member_to_delete) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {
err!("User to delete isn't member of the organization")
};
@@ -1701,7 +1775,7 @@ async fn _delete_member(
.await;
if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await {
nt.send_user_update(UpdateType::SyncOrgKeys, &user).await;
nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await;
}
member_to_delete.delete(conn).await
@@ -1754,7 +1828,7 @@ use super::ciphers::CipherData;
#[serde(rename_all = "camelCase")]
struct ImportData {
ciphers: Vec<CipherData>,
collections: Vec<NewCollectionData>,
collections: Vec<FullCollectionData>,
collection_relationships: Vec<RelationsData>,
}
@@ -1767,16 +1841,20 @@ struct RelationsData {
value: usize,
}
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/ImportCiphersController.cs#L62
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
async fn post_org_import(
query: OrgIdData,
data: Json<ImportData>,
headers: AdminHeaders,
headers: OrgMemberHeaders,
mut conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
let data: ImportData = data.into_inner();
let org_id = query.organization_id;
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let data: ImportData = data.into_inner();
// Validate the import before continuing
// Bitwarden does not process the import if there is one item invalid.
@@ -1789,8 +1867,20 @@ async fn post_org_import(
let mut collections: Vec<CollectionId> = Vec::with_capacity(data.collections.len());
for col in data.collections {
let collection_uuid = if existing_collections.contains(&col.id) {
col.id.unwrap()
let col_id = col.id.unwrap();
// When not an Owner or Admin, check if the member is allowed to access the collection.
if headers.membership.atype < MembershipType::Admin
&& !Collection::can_access_collection(&headers.membership, &col_id, &mut conn).await
{
err!(Compact, "The current user isn't allowed to manage this collection")
}
col_id
} else {
// We do not allow users or managers which can not manage all collections to create new collections
// If there is any collection other than an existing import collection, abort the import.
if headers.membership.atype <= MembershipType::Manager && !headers.membership.has_full_access() {
err!(Compact, "The current user isn't allowed to create new collections")
}
let new_collection = Collection::new(org_id.clone(), col.name, col.external_id);
new_collection.save(&mut conn).await?;
new_collection.uuid
@@ -1813,7 +1903,17 @@ async fn post_org_import(
// Always clear folder_id's via an organization import
cipher_data.folder_id = None;
let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok();
update_cipher_from_data(
&mut cipher,
cipher_data,
&headers,
Some(collections.clone()),
&mut conn,
&nt,
UpdateType::None,
)
.await
.ok();
ciphers.push(cipher.uuid);
}
@@ -1844,12 +1944,6 @@ struct BulkCollectionsData {
async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
let data: BulkCollectionsData = data.into_inner();
// This feature does not seem to be active on all the clients
// To prevent future issues, add a check to block a call when this is set to true
if data.remove_collections {
err!("Bulk removing of collections is not yet implemented")
}
// Get all the collection available to the user in one query
// Also filter based upon the provided collections
let user_collections: HashMap<CollectionId, Collection> =
@@ -1878,8 +1972,16 @@ async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers
// Do not abort the operation just ignore it, it could be a cipher was just deleted for example
if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &mut conn).await {
if cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await {
for collection in &data.collection_ids {
CollectionCipher::save(&cipher.uuid, collection, &mut conn).await?;
// When selecting a specific collection from the left filter list, and use the bulk option, you can remove an item from that collection
// In these cases the client will call this endpoint twice, once for adding the new collections and a second for deleting.
if data.remove_collections {
for collection in &data.collection_ids {
CollectionCipher::delete(&cipher.uuid, collection, &mut conn).await?;
}
} else {
for collection in &data.collection_ids {
CollectionCipher::save(&cipher.uuid, collection, &mut conn).await?;
}
}
}
};
@@ -2356,6 +2458,9 @@ async fn _revoke_member(
headers: &AdminHeaders,
conn: &mut DbConn,
) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
Some(mut member) if member.status > MembershipStatus::Revoked as i32 => {
if member.user_uuid == headers.user.uuid {
@@ -2463,6 +2568,9 @@ async fn _restore_member(
headers: &AdminHeaders,
conn: &mut DbConn,
) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
Some(mut member) if member.status < MembershipStatus::Accepted as i32 => {
if member.user_uuid == headers.user.uuid {
@@ -2510,18 +2618,27 @@ async fn _restore_member(
Ok(())
}
#[get("/organizations/<org_id>/groups")]
async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
async fn get_groups_data(
details: bool,
org_id: OrganizationId,
headers: ManagerHeadersLoose,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
let groups = Group::find_by_organization(&org_id, &mut conn).await;
let mut groups_json = Vec::with_capacity(groups.len());
for g in groups {
groups_json.push(g.to_json_details(&mut conn).await)
if details {
for g in groups {
groups_json.push(g.to_json_details(&mut conn).await)
}
} else {
for g in groups {
groups_json.push(g.to_json())
}
}
groups_json
} else {
@@ -2537,9 +2654,14 @@ async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut co
})))
}
#[get("/organizations/<org_id>/groups")]
async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
get_groups_data(false, org_id, headers, conn).await
}
#[get("/organizations/<org_id>/groups/details", rank = 1)]
async fn get_groups_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
get_groups(org_id, headers, conn).await
get_groups_data(true, org_id, headers, conn).await
}
#[derive(Deserialize)]
@@ -2549,7 +2671,7 @@ struct GroupRequest {
#[serde(default)]
access_all: bool,
external_id: Option<String>,
collections: Vec<SelectedCollection>,
collections: Vec<CollectionData>,
users: Vec<MembershipId>,
}
@@ -2570,14 +2692,14 @@ impl GroupRequest {
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct SelectedCollection {
struct CollectionData {
id: CollectionId,
read_only: bool,
hide_passwords: bool,
manage: bool,
}
impl SelectedCollection {
impl CollectionData {
pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup {
CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage)
}
@@ -2601,6 +2723,9 @@ async fn post_groups(
data: Json<GroupRequest>,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@@ -2630,6 +2755,9 @@ async fn put_group(
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@@ -2660,7 +2788,7 @@ async fn put_group(
async fn add_update_group(
mut group: Group,
collections: Vec<SelectedCollection>,
collections: Vec<CollectionData>,
members: Vec<MembershipId>,
org_id: OrganizationId,
headers: &AdminHeaders,
@@ -2694,7 +2822,8 @@ async fn add_update_group(
"organizationId": group.organizations_uuid,
"name": group.name,
"accessAll": group.access_all,
"externalId": group.external_id
"externalId": group.external_id,
"object": "group"
})))
}
@@ -2745,6 +2874,9 @@ async fn _delete_group(
headers: &AdminHeaders,
conn: &mut DbConn,
) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@@ -2774,6 +2906,9 @@ async fn bulk_delete_groups(
headers: AdminHeaders,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@@ -2837,6 +2972,9 @@ async fn put_group_members(
data: Json<Vec<MembershipId>>,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@@ -3021,7 +3159,7 @@ async fn get_organization_public_key(
headers: OrgMemberHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
@@ -3035,7 +3173,7 @@ async fn get_organization_public_key(
}
// Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients
// https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L487-L492
#[get("/organizations/<org_id>/keys")]
async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
get_organization_public_key(org_id, headers, conn).await
@@ -3086,7 +3224,7 @@ async fn put_reset_password(
user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None);
user.save(&mut conn).await?;
nt.send_logout(&user, None).await;
nt.send_logout(&user, None, &mut conn).await;
log_event(
EventType::OrganizationUserAdminResetPassword as i32,
@@ -3126,16 +3264,16 @@ async fn get_reset_password_details(
check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &mut conn).await?;
// https://github.com/bitwarden/server/blob/3b50ccb9f804efaacdc46bed5b60e5b28eddefcf/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs#L111
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs#L190
Ok(Json(json!({
"object": "organizationUserResetPasswordDetails",
"kdf":user.client_kdf_type,
"kdfIterations":user.client_kdf_iter,
"kdfMemory":user.client_kdf_memory,
"kdfParallelism":user.client_kdf_parallelism,
"resetPasswordKey":member.reset_password_key,
"encryptedPrivateKey":org.private_key,
"organizationUserId": member_id,
"kdf": user.client_kdf_type,
"kdfIterations": user.client_kdf_iter,
"kdfMemory": user.client_kdf_memory,
"kdfParallelism": user.client_kdf_parallelism,
"resetPasswordKey": member.reset_password_key,
"encryptedPrivateKey": org.private_key,
})))
}
@@ -3220,57 +3358,22 @@ async fn put_reset_password_enrollment(
Ok(())
}
// This is a new function active since the v2022.9.x clients.
// It combines the previous two calls done before.
// We call those two functions here and combine them ourselves.
//
// NOTE: It seems clients can't handle uppercase-first keys!!
// We need to convert all keys so they have the first character to be a lowercase.
// Else the export will be just an empty JSON file.
// We currently only support exports by members of the Admin or Owner status.
// Vaultwarden does not yet support exporting only managed collections!
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/OrganizationExportController.cs#L52
#[get("/organizations/<org_id>/export")]
async fn get_org_export(
org_id: OrganizationId,
headers: AdminHeaders,
client_version: Option<ClientVersion>,
mut conn: DbConn,
) -> JsonResult {
async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
// Since version v2023.1.0 the format of the export is different.
// Also, this endpoint was created since v2022.9.0.
// Therefore, we will check for any version smaller then v2023.1.0 and return a different response.
// If we can't determine the version, we will use the latest default v2023.1.0 and higher.
// https://github.com/bitwarden/server/blob/9ca93381ce416454734418c3a9f99ab49747f1b6/src/Api/Controllers/OrganizationExportController.cs#L44
let use_list_response_model = if let Some(client_version) = client_version {
let ver_match = semver::VersionReq::parse("<2023.1.0").unwrap();
ver_match.matches(&client_version.0)
} else {
false
};
// Also both main keys here need to be lowercase, else the export will fail.
if use_list_response_model {
// Backwards compatible pre v2023.1.0 response
Ok(Json(json!({
"collections": {
"data": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await),
"object": "list",
"continuationToken": null,
},
"ciphers": {
"data": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await),
"object": "list",
"continuationToken": null,
}
})))
} else {
// v2023.1.0 and newer response
Ok(Json(json!({
"collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await),
"ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await),
})))
}
Ok(Json(json!({
"collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await),
"ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await),
})))
}
async fn _api_key(
@@ -3280,6 +3383,9 @@ async fn _api_key(
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;

View File

@@ -46,7 +46,7 @@ struct OrgImportData {
#[post("/public/organization/import", data = "<data>")]
async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult {
// Most of the logic for this function can be found here
// https://github.com/bitwarden/server/blob/fd892b2ff4547648a276734fb2b14a8abae2c6f5/src/Core/Services/Implementations/OrganizationService.cs#L1797
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L1203
let org_id = token.0;
let data = data.into_inner();

View File

@@ -2,6 +2,7 @@ use std::path::Path;
use chrono::{DateTime, TimeDelta, Utc};
use num_traits::ToPrimitive;
use once_cell::sync::Lazy;
use rocket::form::Form;
use rocket::fs::NamedFile;
use rocket::fs::TempFile;
@@ -17,6 +18,21 @@ use crate::{
};
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
static ANON_PUSH_DEVICE: Lazy<Device> = Lazy::new(|| {
let dt = crate::util::parse_date("1970-01-01T00:00:00.000000Z");
Device {
uuid: String::from("00000000-0000-0000-0000-000000000000").into(),
created_at: dt,
updated_at: dt,
user_uuid: String::from("00000000-0000-0000-0000-000000000000").into(),
name: String::new(),
atype: 14, // 14 == Unknown Browser
push_uuid: Some(String::from("00000000-0000-0000-0000-000000000000").into()),
push_token: None,
refresh_token: String::new(),
twofactor_remember: None,
}
});
// The max file size allowed by Bitwarden clients and add an extra 5% to avoid issues
const SIZE_525_MB: i64 = 550_502_400;
@@ -182,7 +198,7 @@ async fn post_send(data: Json<SendData>, headers: Headers, mut conn: DbConn, nt:
UpdateType::SyncSendCreate,
&send,
&send.update_users_revision(&mut conn).await,
&headers.device.uuid,
&headers.device,
&mut conn,
)
.await;
@@ -204,6 +220,8 @@ struct UploadDataV2<'f> {
// @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2).
// This method still exists to support older clients, probably need to remove it sometime.
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167
// 2025: This endpoint doesn't seem to exists anymore in the latest version
// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
@@ -272,7 +290,7 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
UpdateType::SyncSendCreate,
&send,
&send.update_users_revision(&mut conn).await,
&headers.device.uuid,
&headers.device,
&mut conn,
)
.await;
@@ -280,7 +298,7 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
Ok(Json(send.to_json()))
}
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L190
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L165
#[post("/sends/file/v2", data = "<data>")]
async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbConn) -> JsonResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
@@ -338,7 +356,7 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
Ok(Json(json!({
"fileUploadType": 0, // 0 == Direct | 1 == Azure
"object": "send-fileUpload",
"url": format!("/sends/{}/file/{}", send.uuid, file_id),
"url": format!("/sends/{}/file/{file_id}", send.uuid),
"sendResponse": send.to_json()
})))
}
@@ -351,7 +369,7 @@ pub struct SendFileData {
fileName: String,
}
// https://github.com/bitwarden/server/blob/66f95d1c443490b653e5a15d32977e2f5a3f9e32/src/Api/Tools/Controllers/SendsController.cs#L250
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L195
#[post("/sends/<send_id>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
async fn post_send_file_v2_data(
send_id: SendId,
@@ -378,7 +396,11 @@ async fn post_send_file_v2_data(
};
match data.data.raw_name() {
Some(raw_file_name) if raw_file_name.dangerous_unsafe_unsanitized_raw() == send_data.fileName => (),
Some(raw_file_name)
if raw_file_name.dangerous_unsafe_unsanitized_raw() == send_data.fileName
// be less strict only if using CLI, cf. https://github.com/dani-garcia/vaultwarden/issues/5614
|| (headers.device.is_cli() && send_data.fileName.ends_with(raw_file_name.dangerous_unsafe_unsanitized_raw().as_str())
) => {}
Some(raw_file_name) => err!(
"Send file name does not match.",
format!(
@@ -420,7 +442,7 @@ async fn post_send_file_v2_data(
UpdateType::SyncSendCreate,
&send,
&send.update_users_revision(&mut conn).await,
&headers.device.uuid,
&headers.device,
&mut conn,
)
.await;
@@ -485,7 +507,7 @@ async fn post_access(
UpdateType::SyncSendUpdate,
&send,
&send.update_users_revision(&mut conn).await,
&String::from("00000000-0000-0000-0000-000000000000").into(),
&ANON_PUSH_DEVICE,
&mut conn,
)
.await;
@@ -542,7 +564,7 @@ async fn post_access_file(
UpdateType::SyncSendUpdate,
&send,
&send.update_users_revision(&mut conn).await,
&String::from("00000000-0000-0000-0000-000000000000").into(),
&ANON_PUSH_DEVICE,
&mut conn,
)
.await;
@@ -552,7 +574,7 @@ async fn post_access_file(
Ok(Json(json!({
"object": "send-fileDownload",
"id": file_id,
"url": format!("{}/api/sends/{}/{}?t={}", &host.host, send_id, file_id, token)
"url": format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host)
})))
}
@@ -641,7 +663,7 @@ pub async fn update_send_from_data(
send.save(conn).await?;
if ut != UpdateType::None {
nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device.uuid, conn).await;
nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device, conn).await;
}
Ok(())
}
@@ -657,7 +679,7 @@ async fn delete_send(send_id: SendId, headers: Headers, mut conn: DbConn, nt: No
UpdateType::SyncSendDelete,
&send,
&send.update_users_revision(&mut conn).await,
&headers.device.uuid,
&headers.device,
&mut conn,
)
.await;
@@ -679,7 +701,7 @@ async fn put_remove_password(send_id: SendId, headers: Headers, mut conn: DbConn
UpdateType::SyncSendUpdate,
&send,
&send.update_users_revision(&mut conn).await,
&headers.device.uuid,
&headers.device,
&mut conn,
)
.await;

View File

@@ -34,6 +34,10 @@ async fn generate_authenticator(data: Json<PasswordOrOtpData>, headers: Headers,
_ => (false, crypto::encode_random_bytes::<20>(BASE32)),
};
// Upstream seems to also return `userVerificationToken`, but doesn't seem to be used at all.
// It should help prevent TOTP disclosure if someone keeps their vault unlocked.
// Since it doesn't seem to be used, and also does not cause any issues, lets leave it out of the response.
// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Controllers/TwoFactorController.cs#L94
Ok(Json(json!({
"enabled": enabled,
"key": key,
@@ -148,7 +152,7 @@ pub async fn validate_totp_code(
if generated == totp_code && time_step > twofactor.last_used {
// If the step does not equals 0 the time is drifted either server or client side.
if step != 0 {
warn!("TOTP Time drift detected. The step offset is {}", step);
warn!("TOTP Time drift detected. The step offset is {step}");
}
// Save the last used time step so only totp time steps higher then this one are allowed.
@@ -157,7 +161,7 @@ pub async fn validate_totp_code(
twofactor.save(conn).await?;
return Ok(());
} else if generated == totp_code && time_step <= twofactor.last_used {
warn!("This TOTP or a TOTP code within {} steps back or forward has already been used!", steps);
warn!("This TOTP or a TOTP code within {steps} steps back or forward has already been used!");
err!(
format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip),
ErrorEvent {

View File

@@ -118,6 +118,9 @@ async fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbCo
} else {
json!({
"enabled": enabled,
"host": null,
"clientSecret": null,
"clientId": null,
"object": "twoFactorDuo"
})
};
@@ -202,7 +205,7 @@ async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData)
use std::str::FromStr;
// https://duo.com/docs/authapi#api-details
let url = format!("https://{}{}", &data.host, path);
let url = format!("https://{}{path}", &data.host);
let date = Utc::now().to_rfc2822();
let username = &data.ik;
let fields = [&date, method, &data.host, path, params];
@@ -274,9 +277,9 @@ pub async fn generate_duo_signature(email: &str, conn: &mut DbConn) -> ApiResult
fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
let val = format!("{email}|{ikey}|{expire}");
let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes()));
let cookie = format!("{prefix}|{}", BASE64.encode(val.as_bytes()));
format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
format!("{cookie}|{}", crypto::hmac_sign(key, &cookie))
}
pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn) -> EmptyResult {

View File

@@ -21,7 +21,7 @@ use url::Url;
// The location on this service that Duo should redirect users to. For us, this is a bridge
// built in to the Bitwarden clients.
// See: https://github.com/bitwarden/clients/blob/main/apps/web/src/connectors/duo-redirect.ts
// See: https://github.com/bitwarden/clients/blob/5fb46df3415aefced0b52f2db86c873962255448/apps/web/src/connectors/duo-redirect.ts
const DUO_REDIRECT_LOCATION: &str = "duo-redirect-connector.html";
// Number of seconds that a JWT we generate for Duo should be valid for.
@@ -182,7 +182,7 @@ impl DuoClient {
HealthCheckResponse::HealthFail {
message,
message_detail,
} => err!(format!("Duo health check FAIL response, msg: {}, detail: {}", message, message_detail)),
} => err!(format!("Duo health check FAIL response, msg: {message}, detail: {message_detail}")),
};
if health_stat != "OK" {
@@ -275,7 +275,7 @@ impl DuoClient {
let status_code = res.status();
if status_code != StatusCode::OK {
err!(format!("Failure response from Duo: {}", status_code))
err!(format!("Failure response from Duo: {status_code}"))
}
let response: IdTokenResponse = match res.json::<IdTokenResponse>().await {
@@ -478,7 +478,7 @@ pub async fn validate_duo_login(
Err(e) => return Err(e),
};
let d: Digest = digest(&SHA512_256, format!("{}{}", ctx.nonce, device_identifier).as_bytes());
let d: Digest = digest(&SHA512_256, format!("{}{device_identifier}", ctx.nonce).as_bytes());
let hash: String = HEXLOWER.encode(d.as_ref());
match client.exchange_authz_code_for_result(code, email, hash.as_str()).await {

View File

@@ -197,14 +197,20 @@ async fn email(data: Json<EmailData>, headers: Headers, mut conn: DbConn) -> Jso
}
/// Validate the email code when used as TwoFactor token mechanism
pub async fn validate_email_code_str(user_id: &UserId, token: &str, data: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn validate_email_code_str(
user_id: &UserId,
token: &str,
data: &str,
ip: &std::net::IpAddr,
conn: &mut DbConn,
) -> EmptyResult {
let mut email_data = EmailTokenData::from_json(data)?;
let mut twofactor = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Email as i32, conn)
.await
.map_res("Two factor not found")?;
let Some(issued_token) = &email_data.last_token else {
err!(
"No token available",
format!("No token available! IP: {ip}"),
ErrorEvent {
event: EventType::UserFailedLogIn2fa
}
@@ -220,7 +226,7 @@ pub async fn validate_email_code_str(user_id: &UserId, token: &str, data: &str,
twofactor.save(conn).await?;
err!(
"Token is invalid",
format!("Token is invalid! IP: {ip}"),
ErrorEvent {
event: EventType::UserFailedLogIn2fa
}
@@ -323,7 +329,7 @@ pub fn obscure_email(email: &str) -> String {
}
};
format!("{}@{}", new_name, &domain)
format!("{new_name}@{domain}")
}
pub async fn find_and_activate_email_2fa(user_id: &UserId, conn: &mut DbConn) -> EmptyResult {

View File

@@ -63,15 +63,18 @@ static CLIENT: Lazy<Client> = Lazy::new(|| {
// Build Regex only once since this takes a lot of time.
static ICON_SIZE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap());
// The function name `icon_external` is checked in the `on_response` function in `AppHeaders`
// It is used to prevent sending a specific header which breaks icon downloads.
// If this function needs to be renamed, also adjust the code in `util.rs`
#[get("/<domain>/icon.png")]
fn icon_external(domain: &str) -> Option<Redirect> {
if !is_valid_domain(domain) {
warn!("Invalid domain: {}", domain);
warn!("Invalid domain: {domain}");
return None;
}
if should_block_address(domain) {
warn!("Blocked address: {}", domain);
warn!("Blocked address: {domain}");
return None;
}
@@ -93,7 +96,7 @@ async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> {
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
if !is_valid_domain(domain) {
warn!("Invalid domain: {}", domain);
warn!("Invalid domain: {domain}");
return Cached::ttl(
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
CONFIG.icon_cache_negttl(),
@@ -102,7 +105,7 @@ async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> {
}
if should_block_address(domain) {
warn!("Blocked address: {}", domain);
warn!("Blocked address: {domain}");
return Cached::ttl(
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
CONFIG.icon_cache_negttl(),
@@ -127,7 +130,7 @@ fn is_valid_domain(domain: &str) -> bool {
// If parsing the domain fails using Url, it will not work with reqwest.
if let Err(parse_error) = url::Url::parse(format!("https://{domain}").as_str()) {
debug!("Domain parse error: '{}' - {:?}", domain, parse_error);
debug!("Domain parse error: '{domain}' - {parse_error:?}");
return false;
} else if domain.is_empty()
|| domain.contains("..")
@@ -136,18 +139,17 @@ fn is_valid_domain(domain: &str) -> bool {
|| domain.ends_with('-')
{
debug!(
"Domain validation error: '{}' is either empty, contains '..', starts with an '.', starts or ends with a '-'",
domain
"Domain validation error: '{domain}' is either empty, contains '..', starts with an '.', starts or ends with a '-'"
);
return false;
} else if domain.len() > 255 {
debug!("Domain validation error: '{}' exceeds 255 characters", domain);
debug!("Domain validation error: '{domain}' exceeds 255 characters");
return false;
}
for c in domain.chars() {
if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {
debug!("Domain validation error: '{}' contains an invalid character '{}'", domain, c);
debug!("Domain validation error: '{domain}' contains an invalid character '{c}'");
return false;
}
}
@@ -156,7 +158,7 @@ fn is_valid_domain(domain: &str) -> bool {
}
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain);
let path = format!("{}/{domain}.png", CONFIG.icon_cache_folder());
// Check for expiration of negatively cached copy
if icon_is_negcached(&path).await {
@@ -164,10 +166,7 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
}
if let Some(icon) = get_cached_icon(&path).await {
let icon_type = match get_icon_type(&icon) {
Some(x) => x,
_ => "x-icon",
};
let icon_type = get_icon_type(&icon).unwrap_or("x-icon");
return Some((icon, icon_type.to_string()));
}
@@ -189,7 +188,7 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
return None;
}
warn!("Unable to download icon: {:?}", e);
warn!("Unable to download icon: {e:?}");
let miss_indicator = path + ".miss";
save_icon(&miss_indicator, &[]).await;
None
@@ -231,7 +230,7 @@ async fn icon_is_negcached(path: &str) -> bool {
// No longer negatively cached, drop the marker
Ok(true) => {
if let Err(e) = remove_file(&miss_indicator).await {
error!("Could not remove negative cache indicator for icon {:?}: {:?}", path, e);
error!("Could not remove negative cache indicator for icon {path:?}: {e:?}");
}
false
}
@@ -531,10 +530,10 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
// Check if the icon type is allowed, else try an icon from the list.
icon_type = get_icon_type(&body);
if icon_type.is_none() {
debug!("Icon from {} data:image uri, is not a valid image type", domain);
debug!("Icon from {domain} data:image uri, is not a valid image type");
continue;
}
info!("Extracted icon from data:image uri for {}", domain);
info!("Extracted icon from data:image uri for {domain}");
buffer = body.freeze();
break;
}
@@ -574,7 +573,7 @@ async fn save_icon(path: &str, icon: &[u8]) {
create_dir_all(&CONFIG.icon_cache_folder()).await.expect("Error creating icon cache folder");
}
Err(e) => {
warn!("Unable to save icon: {:?}", e);
warn!("Unable to save icon: {e:?}");
}
}
}

View File

@@ -17,18 +17,23 @@ use crate::{
push::register_push_device,
ApiResult, EmptyResult, JsonResult,
},
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp},
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp, ClientVersion},
db::{models::*, DbConn},
error::MapResult,
mail, util, CONFIG,
};
pub fn routes() -> Vec<Route> {
routes![login, prelogin, identity_register]
routes![login, prelogin, identity_register, register_verification_email, register_finish]
}
#[post("/connect/token", data = "<data>")]
async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult {
async fn login(
data: Form<ConnectData>,
client_header: ClientHeaders,
client_version: Option<ClientVersion>,
mut conn: DbConn,
) -> JsonResult {
let data: ConnectData = data.into_inner();
let mut user_id: Option<UserId> = None;
@@ -48,7 +53,7 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
_check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?;
_password_login(data, &mut user_id, &mut conn, &client_header.ip).await
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
}
"client_credentials" => {
_check_is_some(&data.client_id, "client_id cannot be blank")?;
@@ -112,7 +117,7 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
device.save(conn).await?;
let result = json!({
@@ -144,6 +149,7 @@ async fn _password_login(
user_id: &mut Option<UserId>,
conn: &mut DbConn,
ip: &ClientIp,
client_version: &Option<ClientVersion>,
) -> JsonResult {
// Validate scope
let scope = data.scope.as_ref().unwrap();
@@ -158,7 +164,7 @@ async fn _password_login(
// Get the user
let username = data.username.as_ref().unwrap().trim();
let Some(mut user) = User::find_by_mail(username, conn).await else {
err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username))
err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {username}.", ip.ip))
};
// Set the user_id here to be passed back used for event logging.
@@ -168,7 +174,7 @@ async fn _password_login(
if !user.enabled {
err!(
"This user has been disabled",
format!("IP: {}. Username: {}.", ip.ip, username),
format!("IP: {}. Username: {username}.", ip.ip),
ErrorEvent {
event: EventType::UserFailedLogIn
}
@@ -182,7 +188,7 @@ async fn _password_login(
let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_id, &user.uuid, conn).await else {
err!(
"Auth request not found. Try again.",
format!("IP: {}. Username: {}.", ip.ip, username),
format!("IP: {}. Username: {username}.", ip.ip),
ErrorEvent {
event: EventType::UserFailedLogIn,
}
@@ -200,7 +206,7 @@ async fn _password_login(
{
err!(
"Username or access code is incorrect. Try again",
format!("IP: {}. Username: {}.", ip.ip, username),
format!("IP: {}. Username: {username}.", ip.ip),
ErrorEvent {
event: EventType::UserFailedLogIn,
}
@@ -209,7 +215,7 @@ async fn _password_login(
} else if !user.check_valid_password(password) {
err!(
"Username or password is incorrect. Try again",
format!("IP: {}. Username: {}.", ip.ip, username),
format!("IP: {}. Username: {username}.", ip.ip),
ErrorEvent {
event: EventType::UserFailedLogIn,
}
@@ -222,7 +228,7 @@ async fn _password_login(
user.set_password(password, None, false, None);
if let Err(e) = user.save(conn).await {
error!("Error updating user: {:#?}", e);
error!("Error updating user: {e:#?}");
}
}
@@ -241,11 +247,11 @@ async fn _password_login(
user.login_verify_count += 1;
if let Err(e) = user.save(conn).await {
error!("Error updating user: {:#?}", e);
error!("Error updating user: {e:#?}");
}
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {
error!("Error auto-sending email verification email: {:#?}", e);
error!("Error auto-sending email verification email: {e:#?}");
}
}
}
@@ -253,7 +259,7 @@ async fn _password_login(
// We still want the login to fail until they actually verified the email address
err!(
"Please verify your email before trying again.",
format!("IP: {}. Username: {}.", ip.ip, username),
format!("IP: {}. Username: {username}.", ip.ip),
ErrorEvent {
event: EventType::UserFailedLogIn
}
@@ -262,11 +268,11 @@ async fn _password_login(
let (mut device, new_device) = get_device(&data, conn, &user).await;
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?;
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?;
if CONFIG.mail_enabled() && new_device {
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
error!("Error sending new device email: {:#?}", e);
error!("Error sending new device email: {e:#?}");
if CONFIG.require_device_email() {
err!(
@@ -291,10 +297,10 @@ async fn _password_login(
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
device.save(conn).await?;
// Fetch all valid Master Password Policies and merge them into one with all true's and larges numbers as one policy
// Fetch all valid Master Password Policies and merge them into one with all trues and largest numbers as one policy
let master_password_policies: Vec<MasterPasswordPolicy> =
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(
&user.uuid,
@@ -306,6 +312,7 @@ async fn _password_login(
.filter_map(|p| serde_json::from_str(&p.data).ok())
.collect();
// NOTE: Upstream still uses PascalCase here for `Object`!
let master_password_policy = if !master_password_policies.is_empty() {
let mut mpp_json = json!(master_password_policies.into_iter().reduce(|acc, policy| {
MasterPasswordPolicy {
@@ -318,10 +325,10 @@ async fn _password_login(
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
}
}));
mpp_json["object"] = json!("masterPasswordPolicy");
mpp_json["Object"] = json!("masterPasswordPolicy");
mpp_json
} else {
json!({"object": "masterPasswordPolicy"})
json!({"Object": "masterPasswordPolicy"})
};
let mut result = json!({
@@ -352,7 +359,7 @@ async fn _password_login(
result["TwoFactorToken"] = Value::String(token);
}
info!("User {} logged in successfully. IP: {}", username, ip.ip);
info!("User {username} logged in successfully. IP: {}", ip.ip);
Ok(Json(result))
}
@@ -420,7 +427,7 @@ async fn _user_api_key_login(
if CONFIG.mail_enabled() && new_device {
let now = Utc::now().naive_utc();
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
error!("Error sending new device email: {:#?}", e);
error!("Error sending new device email: {e:#?}");
if CONFIG.require_device_email() {
err!(
@@ -441,7 +448,7 @@ async fn _user_api_key_login(
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
device.save(conn).await?;
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
@@ -520,6 +527,7 @@ async fn twofactor_auth(
data: &ConnectData,
device: &mut Device,
ip: &ClientIp,
client_version: &Option<ClientVersion>,
conn: &mut DbConn,
) -> ApiResult<Option<String>> {
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
@@ -538,7 +546,10 @@ async fn twofactor_auth(
let twofactor_code = match data.two_factor_token {
Some(ref code) => code,
None => {
err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?, "2FA token not provided")
err_json!(
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"2FA token not provided"
)
}
};
@@ -575,7 +586,7 @@ async fn twofactor_auth(
}
}
Some(TwoFactorType::Email) => {
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, conn).await?
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?
}
Some(TwoFactorType::Remember) => {
@@ -585,7 +596,7 @@ async fn twofactor_auth(
}
_ => {
err_json!(
_json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?,
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"2FA Remember token not provided"
)
}
@@ -617,6 +628,7 @@ async fn _json_err_twofactor(
providers: &[i32],
user_id: &UserId,
data: &ConnectData,
client_version: &Option<ClientVersion>,
conn: &mut DbConn,
) -> ApiResult<Value> {
let mut result = json!({
@@ -689,8 +701,16 @@ async fn _json_err_twofactor(
err!("No twofactor email registered")
};
// Send email immediately if email is the only 2FA option
if providers.len() == 1 {
// Starting with version 2025.5.0 the client will call `/api/two-factor/send-email-login`.
let disabled_send = if let Some(cv) = client_version {
let ver_match = semver::VersionReq::parse(">=2025.5.0").unwrap();
ver_match.matches(&cv.0)
} else {
false
};
// Send email immediately if email is the only 2FA option.
if providers.len() == 1 && !disabled_send {
email::send_token(user_id, conn).await?
}
@@ -714,7 +734,66 @@ async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
#[post("/accounts/register", data = "<data>")]
async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
_register(data, conn).await
_register(data, false, conn).await
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RegisterVerificationData {
email: String,
name: Option<String>,
// receiveMarketingEmails: bool,
}
#[derive(rocket::Responder)]
enum RegisterVerificationResponse {
NoContent(()),
Token(Json<String>),
}
#[post("/accounts/register/send-verification-email", data = "<data>")]
async fn register_verification_email(
data: Json<RegisterVerificationData>,
mut conn: DbConn,
) -> ApiResult<RegisterVerificationResponse> {
let data = data.into_inner();
if !CONFIG.is_signup_allowed(&data.email) {
err!("Registration not allowed or user already exists")
}
let should_send_mail = CONFIG.mail_enabled() && CONFIG.signups_verify();
let token_claims =
crate::auth::generate_register_verify_claims(data.email.clone(), data.name.clone(), should_send_mail);
let token = crate::auth::encode_jwt(&token_claims);
if should_send_mail {
let user = User::find_by_mail(&data.email, &mut conn).await;
if user.filter(|u| u.private_key.is_some()).is_some() {
// There is still a timing side channel here in that the code
// paths that send mail take noticeably longer than ones that
// don't. Add a randomized sleep to mitigate this somewhat.
use rand::{rngs::SmallRng, Rng, SeedableRng};
let mut rng = SmallRng::from_os_rng();
let delta: i32 = 100;
let sleep_ms = (1_000 + rng.random_range(-delta..=delta)) as u64;
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
} else {
mail::send_register_verify_email(&data.email, &token).await?;
}
Ok(RegisterVerificationResponse::NoContent(()))
} else {
// If email verification is not required, return the token directly
// the clients will use this token to finish the registration
Ok(RegisterVerificationResponse::Token(Json(token)))
}
}
#[post("/accounts/register/finish", data = "<data>")]
async fn register_finish(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
_register(data, true, conn).await
}
// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts

View File

@@ -10,7 +10,7 @@ use rocket_ws::{Message, WebSocket};
use crate::{
auth::{ClientIp, WsAccessTokenHeader},
db::{
models::{AuthRequestId, Cipher, CollectionId, DeviceId, Folder, Send as DbSend, User, UserId},
models::{AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, PushId, Send as DbSend, User, UserId},
DbConn,
},
Error, CONFIG,
@@ -339,7 +339,7 @@ impl WebSocketUsers {
}
// NOTE: The last modified date needs to be updated before calling these methods
pub async fn send_user_update(&self, ut: UpdateType, user: &User) {
pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &mut DbConn) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
return;
@@ -355,11 +355,11 @@ impl WebSocketUsers {
}
if CONFIG.push_enabled() {
push_user_update(ut, user);
push_user_update(ut, user, push_uuid, conn).await;
}
}
pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>) {
pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>, conn: &mut DbConn) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
return;
@@ -375,17 +375,11 @@ impl WebSocketUsers {
}
if CONFIG.push_enabled() {
push_logout(user, acting_device_id.clone());
push_logout(user, acting_device_id.clone(), conn).await;
}
}
pub async fn send_folder_update(
&self,
ut: UpdateType,
folder: &Folder,
acting_device_id: &DeviceId,
conn: &mut DbConn,
) {
pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder, device: &Device, conn: &mut DbConn) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
return;
@@ -397,7 +391,7 @@ impl WebSocketUsers {
("RevisionDate".into(), serialize_date(folder.updated_at)),
],
ut,
Some(acting_device_id.clone()),
Some(device.uuid.clone()),
);
if CONFIG.enable_websocket() {
@@ -405,7 +399,7 @@ impl WebSocketUsers {
}
if CONFIG.push_enabled() {
push_folder_update(ut, folder, acting_device_id, conn).await;
push_folder_update(ut, folder, device, conn).await;
}
}
@@ -414,7 +408,7 @@ impl WebSocketUsers {
ut: UpdateType,
cipher: &Cipher,
user_ids: &[UserId],
acting_device_id: &DeviceId,
device: &Device,
collection_uuids: Option<Vec<CollectionId>>,
conn: &mut DbConn,
) {
@@ -444,7 +438,7 @@ impl WebSocketUsers {
("RevisionDate".into(), revision_date),
],
ut,
Some(acting_device_id.clone()),
Some(device.uuid.clone()), // Acting device id (unique device/app uuid)
);
if CONFIG.enable_websocket() {
@@ -454,7 +448,7 @@ impl WebSocketUsers {
}
if CONFIG.push_enabled() && user_ids.len() == 1 {
push_cipher_update(ut, cipher, acting_device_id, conn).await;
push_cipher_update(ut, cipher, device, conn).await;
}
}
@@ -463,7 +457,7 @@ impl WebSocketUsers {
ut: UpdateType,
send: &DbSend,
user_ids: &[UserId],
acting_device_id: &DeviceId,
device: &Device,
conn: &mut DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
@@ -488,15 +482,15 @@ impl WebSocketUsers {
}
}
if CONFIG.push_enabled() && user_ids.len() == 1 {
push_send_update(ut, send, acting_device_id, conn).await;
push_send_update(ut, send, device, conn).await;
}
}
pub async fn send_auth_request(
&self,
user_id: &UserId,
auth_request_uuid: &String,
acting_device_id: &DeviceId,
auth_request_uuid: &str,
device: &Device,
conn: &mut DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
@@ -504,16 +498,16 @@ impl WebSocketUsers {
return;
}
let data = create_update(
vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_id.to_string().into())],
vec![("Id".into(), auth_request_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())],
UpdateType::AuthRequest,
Some(acting_device_id.clone()),
Some(device.uuid.clone()),
);
if CONFIG.enable_websocket() {
self.send_update(user_id, &data).await;
}
if CONFIG.push_enabled() {
push_auth_request(user_id.clone(), auth_request_uuid.to_string(), conn).await;
push_auth_request(user_id, auth_request_uuid, device, conn).await;
}
}
@@ -521,7 +515,7 @@ impl WebSocketUsers {
&self,
user_id: &UserId,
auth_request_id: &AuthRequestId,
approving_device_id: &DeviceId,
device: &Device,
conn: &mut DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
@@ -531,14 +525,14 @@ impl WebSocketUsers {
let data = create_update(
vec![("Id".into(), auth_request_id.to_string().into()), ("UserId".into(), user_id.to_string().into())],
UpdateType::AuthRequestResponse,
Some(approving_device_id.clone()),
Some(device.uuid.clone()),
);
if CONFIG.enable_websocket() {
self.send_update(user_id, &data).await;
}
if CONFIG.push_enabled() {
push_auth_response(user_id, auth_request_id, approving_device_id, conn).await;
push_auth_response(user_id, auth_request_id, device, conn).await;
}
}
}

View File

@@ -7,9 +7,9 @@ use tokio::sync::RwLock;
use crate::{
api::{ApiResult, EmptyResult, UpdateType},
db::models::{AuthRequestId, Cipher, Device, DeviceId, Folder, Send, User, UserId},
db::models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId},
http_client::make_http_request,
util::format_date,
util::{format_date, get_uuid},
CONFIG,
};
@@ -28,20 +28,20 @@ struct LocalAuthPushToken {
valid_until: Instant,
}
async fn get_auth_push_token() -> ApiResult<String> {
static PUSH_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
async fn get_auth_api_token() -> ApiResult<String> {
static API_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
RwLock::new(LocalAuthPushToken {
access_token: String::new(),
valid_until: Instant::now(),
})
});
let push_token = PUSH_TOKEN.read().await;
let api_token = API_TOKEN.read().await;
if push_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {
if api_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {
debug!("Auth Push token still valid, no need for a new one");
return Ok(push_token.access_token.clone());
return Ok(api_token.access_token.clone());
}
drop(push_token); // Drop the read lock now
drop(api_token); // Drop the read lock now
let installation_id = CONFIG.push_installation_id();
let client_id = format!("installation.{installation_id}");
@@ -68,44 +68,48 @@ async fn get_auth_push_token() -> ApiResult<String> {
Err(e) => err!(format!("Unexpected push token received from bitwarden server: {e}")),
};
let mut push_token = PUSH_TOKEN.write().await;
push_token.valid_until = Instant::now()
let mut api_token = API_TOKEN.write().await;
api_token.valid_until = Instant::now()
.checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time
.unwrap();
push_token.access_token = json_pushtoken.access_token;
api_token.access_token = json_pushtoken.access_token;
debug!("Token still valid for {}", push_token.valid_until.saturating_duration_since(Instant::now()).as_secs());
Ok(push_token.access_token.clone())
debug!("Token still valid for {}", api_token.valid_until.saturating_duration_since(Instant::now()).as_secs());
Ok(api_token.access_token.clone())
}
pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbConn) -> EmptyResult {
if !CONFIG.push_enabled() || !device.is_push_device() || device.is_registered() {
if !CONFIG.push_enabled() || !device.is_push_device() {
return Ok(());
}
if device.push_token.is_none() {
warn!("Skipping the registration of the device {} because the push_token field is empty.", device.uuid);
warn!("To get rid of this message you need to clear the app data and reconnect the device.");
warn!("Skipping the registration of the device {:?} because the push_token field is empty.", device.uuid);
warn!("To get rid of this message you need to logout, clear the app data and login again on the device.");
return Ok(());
}
debug!("Registering Device {}", device.uuid);
debug!("Registering Device {:?}", device.push_uuid);
// generate a random push_uuid so we know the device is registered
device.push_uuid = Some(uuid::Uuid::new_v4().to_string());
// Generate a random push_uuid so if it doesn't already have one
if device.push_uuid.is_none() {
device.push_uuid = Some(PushId(get_uuid()));
}
//Needed to register a device for push to bitwarden :
let data = json!({
"deviceId": device.push_uuid, // Unique UUID per user/device
"pushToken": device.push_token,
"userId": device.user_uuid,
"deviceId": device.push_uuid,
"identifier": device.uuid,
"type": device.atype,
"pushToken": device.push_token
"identifier": device.uuid, // Unique UUID of the device/app, determined by the device/app it self currently registering
// "organizationIds:" [] // TODO: This is not yet implemented by Vaultwarden!
"installationId": CONFIG.push_installation_id(),
});
let auth_push_token = get_auth_push_token().await?;
let auth_header = format!("Bearer {}", &auth_push_token);
let auth_api_token = get_auth_api_token().await?;
let auth_header = format!("Bearer {auth_api_token}");
if let Err(e) = make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/register"))?
.header(CONTENT_TYPE, "application/json")
@@ -126,18 +130,21 @@ pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbC
Ok(())
}
pub async fn unregister_push_device(push_id: Option<String>) -> EmptyResult {
pub async fn unregister_push_device(push_id: &Option<PushId>) -> EmptyResult {
if !CONFIG.push_enabled() || push_id.is_none() {
return Ok(());
}
let auth_push_token = get_auth_push_token().await?;
let auth_api_token = get_auth_api_token().await?;
let auth_header = format!("Bearer {}", &auth_push_token);
let auth_header = format!("Bearer {auth_api_token}");
match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_id.unwrap()))?
.header(AUTHORIZATION, auth_header)
.send()
.await
match make_http_request(
Method::POST,
&format!("{}/push/delete/{}", CONFIG.push_relay_uri(), push_id.as_ref().unwrap()),
)?
.header(AUTHORIZATION, auth_header)
.send()
.await
{
Ok(r) => r,
Err(e) => err!(format!("An error occurred during device unregistration: {e}")),
@@ -145,12 +152,7 @@ pub async fn unregister_push_device(push_id: Option<String>) -> EmptyResult {
Ok(())
}
pub async fn push_cipher_update(
ut: UpdateType,
cipher: &Cipher,
acting_device_id: &DeviceId,
conn: &mut crate::db::DbConn,
) {
pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device, conn: &mut crate::db::DbConn) {
// We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.
if cipher.organization_uuid.is_some() {
return;
@@ -163,87 +165,97 @@ pub async fn push_cipher_update(
if Device::check_user_has_push_device(user_id, conn).await {
send_to_push_relay(json!({
"userId": user_id,
"organizationId": (),
"deviceId": acting_device_id,
"identifier": acting_device_id,
"organizationId": null,
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
"type": ut as i32,
"payload": {
"Id": cipher.uuid,
"UserId": cipher.user_uuid,
"OrganizationId": (),
"RevisionDate": format_date(&cipher.updated_at)
}
"id": cipher.uuid,
"userId": cipher.user_uuid,
"organizationId": null,
"collectionIds": null,
"revisionDate": format_date(&cipher.updated_at)
},
"clientType": null,
"installationId": null
}))
.await;
}
}
pub fn push_logout(user: &User, acting_device_id: Option<DeviceId>) {
pub async fn push_logout(user: &User, acting_device_id: Option<DeviceId>, conn: &mut crate::db::DbConn) {
let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null);
tokio::task::spawn(send_to_push_relay(json!({
"userId": user.uuid,
"organizationId": (),
"deviceId": acting_device_id,
"identifier": acting_device_id,
"type": UpdateType::LogOut as i32,
"payload": {
"UserId": user.uuid,
"Date": format_date(&user.updated_at)
}
})));
}
pub fn push_user_update(ut: UpdateType, user: &User) {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user.uuid,
"organizationId": (),
"deviceId": (),
"identifier": (),
"type": ut as i32,
"payload": {
"UserId": user.uuid,
"Date": format_date(&user.updated_at)
}
})));
}
pub async fn push_folder_update(
ut: UpdateType,
folder: &Folder,
acting_device_id: &DeviceId,
conn: &mut crate::db::DbConn,
) {
if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
if Device::check_user_has_push_device(&user.uuid, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": folder.user_uuid,
"userId": user.uuid,
"organizationId": (),
"deviceId": acting_device_id,
"identifier": acting_device_id,
"type": ut as i32,
"type": UpdateType::LogOut as i32,
"payload": {
"Id": folder.uuid,
"UserId": folder.user_uuid,
"RevisionDate": format_date(&folder.updated_at)
}
"userId": user.uuid,
"date": format_date(&user.updated_at)
},
"clientType": null,
"installationId": null
})));
}
}
pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_id: &DeviceId, conn: &mut crate::db::DbConn) {
pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &mut crate::db::DbConn) {
if Device::check_user_has_push_device(&user.uuid, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user.uuid,
"organizationId": null,
"deviceId": push_uuid,
"identifier": null,
"type": ut as i32,
"payload": {
"userId": user.uuid,
"date": format_date(&user.updated_at)
},
"clientType": null,
"installationId": null
})));
}
}
pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device, conn: &mut crate::db::DbConn) {
if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": folder.user_uuid,
"organizationId": null,
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
"type": ut as i32,
"payload": {
"id": folder.uuid,
"userId": folder.user_uuid,
"revisionDate": format_date(&folder.updated_at)
},
"clientType": null,
"installationId": null
})));
}
}
pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &mut crate::db::DbConn) {
if let Some(s) = &send.user_uuid {
if Device::check_user_has_push_device(s, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": send.user_uuid,
"organizationId": (),
"deviceId": acting_device_id,
"identifier": acting_device_id,
"organizationId": null,
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
"type": ut as i32,
"payload": {
"Id": send.uuid,
"UserId": send.user_uuid,
"RevisionDate": format_date(&send.revision_date)
}
"id": send.uuid,
"userId": send.user_uuid,
"revisionDate": format_date(&send.revision_date)
},
"clientType": null,
"installationId": null
})));
}
}
@@ -254,20 +266,20 @@ async fn send_to_push_relay(notification_data: Value) {
return;
}
let auth_push_token = match get_auth_push_token().await {
let auth_api_token = match get_auth_api_token().await {
Ok(s) => s,
Err(e) => {
debug!("Could not get the auth push token: {}", e);
debug!("Could not get the auth push token: {e}");
return;
}
};
let auth_header = format!("Bearer {}", &auth_push_token);
let auth_header = format!("Bearer {auth_api_token}");
let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/send")) {
Ok(r) => r,
Err(e) => {
error!("An error occurred while sending a send update to the push relay: {}", e);
error!("An error occurred while sending a send update to the push relay: {e}");
return;
}
};
@@ -280,22 +292,24 @@ async fn send_to_push_relay(notification_data: Value) {
.send()
.await
{
error!("An error occurred while sending a send update to the push relay: {}", e);
error!("An error occurred while sending a send update to the push relay: {e}");
};
}
pub async fn push_auth_request(user_id: UserId, auth_request_id: String, conn: &mut crate::db::DbConn) {
if Device::check_user_has_push_device(&user_id, conn).await {
pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &mut crate::db::DbConn) {
if Device::check_user_has_push_device(user_id, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user_id,
"organizationId": (),
"deviceId": null,
"identifier": null,
"organizationId": null,
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
"type": UpdateType::AuthRequest as i32,
"payload": {
"Id": auth_request_id,
"UserId": user_id,
}
"userId": user_id,
"id": auth_request_id,
},
"clientType": null,
"installationId": null
})));
}
}
@@ -303,20 +317,22 @@ pub async fn push_auth_request(user_id: UserId, auth_request_id: String, conn: &
pub async fn push_auth_response(
user_id: &UserId,
auth_request_id: &AuthRequestId,
approving_device_id: &DeviceId,
device: &Device,
conn: &mut crate::db::DbConn,
) {
if Device::check_user_has_push_device(user_id, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user_id,
"organizationId": (),
"deviceId": approving_device_id,
"identifier": approving_device_id,
"organizationId": null,
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
"type": UpdateType::AuthRequestResponse as i32,
"payload": {
"Id": auth_request_id,
"UserId": user_id,
}
"userId": user_id,
"id": auth_request_id,
},
"clientType": null,
"installationId": null
})));
}
}

View File

@@ -57,7 +57,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
let css_options = json!({
"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()),
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
"sends_allowed": CONFIG.sends_allowed(),
"load_user_scss": true,

View File

@@ -35,6 +35,7 @@ static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin()));
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin()));
static JWT_REGISTER_VERIFY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|register_verify", CONFIG.domain_origin()));
static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
@@ -145,6 +146,10 @@ pub fn decode_file_download(token: &str) -> Result<FileDownloadClaims, Error> {
decode_jwt(token, JWT_FILE_DOWNLOAD_ISSUER.to_string())
}
pub fn decode_register_verify(token: &str) -> Result<RegisterVerifyClaims, Error> {
decode_jwt(token, JWT_REGISTER_VERIFY_ISSUER.to_string())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginJwtClaims {
// Not before
@@ -176,6 +181,11 @@ pub struct LoginJwtClaims {
pub sstamp: String,
// device uuid
pub device: DeviceId,
// what kind of device, like FirefoxBrowser or Android derived from DeviceType
pub devicetype: String,
// the type of client_id, like web, cli, desktop, browser or mobile
pub client_id: String,
// [ "api", "offline_access" ]
pub scope: Vec<String>,
// [ "Application" ]
@@ -284,7 +294,7 @@ pub fn generate_organization_api_key_login_claims(
exp: (time_now + TimeDelta::try_hours(1).unwrap()).timestamp(),
iss: JWT_ORG_API_KEY_ISSUER.to_string(),
sub: org_api_key_uuid,
client_id: format!("organization.{}", org_id),
client_id: format!("organization.{org_id}"),
client_sub: org_id,
scope: vec!["api.organization".into()],
}
@@ -315,6 +325,33 @@ pub fn generate_file_download_claims(cipher_id: CipherId, file_id: AttachmentId)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RegisterVerifyClaims {
// Not before
pub nbf: i64,
// Expiration time
pub exp: i64,
// Issuer
pub iss: String,
// Subject
pub sub: String,
pub name: Option<String>,
pub verified: bool,
}
pub fn generate_register_verify_claims(email: String, name: Option<String>, verified: bool) -> RegisterVerifyClaims {
let time_now = Utc::now();
RegisterVerifyClaims {
nbf: time_now.timestamp(),
exp: (time_now + TimeDelta::try_minutes(30).unwrap()).timestamp(),
iss: JWT_REGISTER_VERIFY_ISSUER.to_string(),
sub: email,
name,
verified,
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BasicJwtClaims {
// Not before
@@ -515,7 +552,7 @@ impl<'r> FromRequest<'r> for Headers {
let mut user = user;
user.reset_stamp_exception();
if let Err(e) = user.save(&mut conn).await {
error!("Error updating user: {:#?}", e);
error!("Error updating user: {e:#?}");
}
err_handler!("Stamp exception is expired")
} else if !stamp_exception.routes.contains(&current_route.to_string()) {
@@ -657,17 +694,6 @@ impl<'r> FromRequest<'r> for AdminHeaders {
}
}
impl From<AdminHeaders> for Headers {
fn from(h: AdminHeaders) -> Headers {
Headers {
host: h.host,
device: h.device,
user: h.user,
ip: h.ip,
}
}
}
// col_id is usually the fourth path param ("/organizations/<org_id>/collections/<col_id>"),
// but there could be cases where it is a query value.
// First check the path, if this is not a valid uuid, try the query values.
@@ -837,8 +863,10 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
pub struct OrgMemberHeaders {
pub host: String,
pub device: Device,
pub user: User,
pub org_id: OrganizationId,
pub membership: Membership,
pub ip: ClientIp,
}
#[rocket::async_trait]
@@ -850,8 +878,10 @@ impl<'r> FromRequest<'r> for OrgMemberHeaders {
if headers.is_member() {
Outcome::Success(Self {
host: headers.host,
device: headers.device,
user: headers.user,
org_id: headers.membership.org_uuid,
membership: headers.membership,
ip: headers.ip,
})
} else {
err_handler!("You need to be a Member of the Organization to call this endpoint")
@@ -859,6 +889,17 @@ impl<'r> FromRequest<'r> for OrgMemberHeaders {
}
}
impl From<OrgMemberHeaders> for Headers {
fn from(h: OrgMemberHeaders) -> Headers {
Headers {
host: h.host,
device: h.device,
user: h.user,
ip: h.ip,
}
}
}
//
// Client IP address detection
//
@@ -879,7 +920,7 @@ impl<'r> FromRequest<'r> for ClientIp {
None => ip,
}
.parse()
.map_err(|_| warn!("'{}' header is malformed: {}", CONFIG.ip_header(), ip))
.map_err(|_| warn!("'{}' header is malformed: {ip}", CONFIG.ip_header()))
.ok()
})
} else {

View File

@@ -104,7 +104,7 @@ macro_rules! make_config {
let mut builder = ConfigBuilder::default();
$($(
builder.$name = make_config! { @getenv paste::paste!(stringify!([<$name:upper>])), $ty };
builder.$name = make_config! { @getenv pastey::paste!(stringify!([<$name:upper>])), $ty };
)+)+
builder
@@ -133,7 +133,7 @@ macro_rules! make_config {
builder.$name = v.clone();
if self.$name.is_some() {
overrides.push(paste::paste!(stringify!([<$name:upper>])).into());
overrides.push(pastey::paste!(stringify!([<$name:upper>])).into());
}
}
)+)+
@@ -231,7 +231,7 @@ macro_rules! make_config {
element.insert("default".into(), serde_json::to_value(def.$name).unwrap());
element.insert("type".into(), (_get_form_type(stringify!($ty))).into());
element.insert("doc".into(), (_get_doc(concat!($($doc),+))).into());
element.insert("overridden".into(), (overridden.contains(&paste::paste!(stringify!([<$name:upper>])).into())).into());
element.insert("overridden".into(), (overridden.contains(&pastey::paste!(stringify!([<$name:upper>])).into())).into());
element
}),
)+
@@ -375,19 +375,19 @@ make_config! {
/// Data folder |> Main data folder
data_folder: String, false, def, "data".to_string();
/// Database URL
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
database_url: String, false, auto, |c| format!("{}/db.sqlite3", c.data_folder);
/// Icon cache folder
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
icon_cache_folder: String, false, auto, |c| format!("{}/icon_cache", c.data_folder);
/// Attachments folder
attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
attachments_folder: String, false, auto, |c| format!("{}/attachments", c.data_folder);
/// Sends folder
sends_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "sends");
sends_folder: String, false, auto, |c| format!("{}/sends", c.data_folder);
/// Temp folder |> Used for storing temporary file uploads
tmp_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "tmp");
tmp_folder: String, false, auto, |c| format!("{}/tmp", c.data_folder);
/// Templates folder
templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
templates_folder: String, false, auto, |c| format!("{}/templates", c.data_folder);
/// Session JWT key
rsa_key_filename: String, false, auto, |c| format!("{}/{}", c.data_folder, "rsa_key");
rsa_key_filename: String, false, auto, |c| format!("{}/rsa_key", c.data_folder);
/// Web vault folder
web_vault_folder: String, false, def, "web-vault/".to_string();
},
@@ -484,7 +484,8 @@ make_config! {
disable_icon_download: bool, true, def, false;
/// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled
signups_allowed: bool, true, def, true;
/// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
/// Require email verification on signups. On new client versions, this will require verification at signup time. On older clients,
/// this will prevent logins from succeeding until the address has been verified
signups_verify: bool, true, def, false;
/// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
signups_verify_resend_time: u64, true, def, 3_600;
@@ -578,7 +579,7 @@ make_config! {
authenticator_disable_time_drift: bool, true, def, false;
/// Customize the enabled feature flags on the clients |> This is a comma separated list of feature flags to enable.
experimental_client_feature_flags: String, false, def, "fido2-vault-credentials".to_string();
experimental_client_feature_flags: String, false, def, String::new();
/// Require new device emails |> When a user logs in an email is required to be sent.
/// If sending the email fails the login attempt will fail.
@@ -734,7 +735,7 @@ make_config! {
email_expiration_time: u64, true, def, 600;
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
email_attempts_limit: u64, true, def, 3;
/// Automatically enforce at login |> Setup email 2FA provider regardless of any organization policy
/// Setup email 2FA at signup |> Setup email 2FA provider on registration regardless of any organization policy
email_2fa_enforce_on_verified_invite: bool, true, def, false;
/// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed
email_2fa_auto_fallback: bool, true, def, false;
@@ -832,16 +833,25 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
}
}
// TODO: deal with deprecated flags so they can be removed from this list, cf. #4263
// Server (v2025.5.0): https://github.com/bitwarden/server/blob/4a7db112a0952c6df8bacf36c317e9c4e58c3651/src/Core/Constants.cs#L102
// Client (v2025.5.0): https://github.com/bitwarden/clients/blob/9df8a3cc50ed45f52513e62c23fcc8a4b745f078/libs/common/src/enums/feature-flag.enum.ts#L10
// Android (v2025.4.0): https://github.com/bitwarden/android/blob/bee09de972c3870de0d54a0067996be473ec55c7/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L27
// iOS (v2025.4.0): https://github.com/bitwarden/ios/blob/956e05db67344c912e3a1b8cb2609165d67da1c9/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
//
// NOTE: Move deprecated flags to the utils::parse_experimental_client_feature_flags() DEPRECATED_FLAGS const!
const KNOWN_FLAGS: &[&str] = &[
"autofill-overlay",
"autofill-v2",
"browser-fileless-import",
"extension-refresh",
"fido2-vault-credentials",
// Autofill Team
"inline-menu-positioning-improvements",
"ssh-key-vault-item",
"inline-menu-totp",
"ssh-agent",
// Key Management Team
"ssh-key-vault-item",
// Tools
"export-attachments",
// Mobile Team
"anon-addy-self-host-alias",
"simple-login-self-host-alias",
"mutual-tls",
];
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
@@ -1210,7 +1220,7 @@ impl Config {
pub fn is_email_domain_allowed(&self, email: &str) -> bool {
let e: Vec<&str> = email.rsplitn(2, '@').collect();
if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {
warn!("Failed to parse email address '{}'", email);
warn!("Failed to parse email address '{email}'");
return false;
}
let email_domain = e[0].to_lowercase();
@@ -1367,6 +1377,7 @@ where
reg!("email/email_footer_text");
reg!("email/admin_reset_password", ".html");
reg!("email/change_email_existing", ".html");
reg!("email/change_email", ".html");
reg!("email/delete_account", ".html");
reg!("email/emergency_access_invite_accepted", ".html");
@@ -1383,6 +1394,7 @@ where
reg!("email/protected_action", ".html");
reg!("email/pw_hint_none", ".html");
reg!("email/pw_hint_some", ".html");
reg!("email/register_verify_email", ".html");
reg!("email/send_2fa_removed_from_org", ".html");
reg!("email/send_emergency_access_invite", ".html");
reg!("email/send_org_invite", ".html");

View File

@@ -110,7 +110,6 @@ pub fn generate_api_key() -> String {
// Constant time compare
//
pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {
use ring::constant_time::verify_slices_are_equal;
verify_slices_are_equal(a.as_ref(), b.as_ref()).is_ok()
use subtle::ConstantTimeEq;
a.as_ref().ct_eq(b.as_ref()).into()
}

View File

@@ -130,7 +130,7 @@ macro_rules! generate_connections {
DbConnType::$name => {
#[cfg($name)]
{
paste::paste!{ [< $name _migrations >]::run_migrations()?; }
pastey::paste!{ [< $name _migrations >]::run_migrations()?; }
let manager = ConnectionManager::new(&url);
let pool = Pool::builder()
.max_size(CONFIG.database_max_conns())
@@ -259,7 +259,7 @@ macro_rules! db_run {
$($(
#[cfg($db)]
$crate::db::DbConnInner::$db($conn) => {
paste::paste! {
pastey::paste! {
#[allow(unused)] use $crate::db::[<__ $db _schema>]::{self as schema, *};
#[allow(unused)] use [<__ $db _model>]::*;
}
@@ -280,7 +280,7 @@ macro_rules! db_run {
$($(
#[cfg($db)]
$crate::db::DbConnInner::$db($conn) => {
paste::paste! {
pastey::paste! {
#[allow(unused)] use $crate::db::[<__ $db _schema>]::{self as schema, *};
// @ RAW: #[allow(unused)] use [<__ $db _model>]::*;
}
@@ -337,7 +337,7 @@ macro_rules! db_object {
};
( @db $db:ident | $( #[$attr:meta] )* | $name:ident | $( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty),+) => {
paste::paste! {
pastey::paste! {
#[allow(unused)] use super::*;
#[allow(unused)] use diesel::prelude::*;
#[allow(unused)] use $crate::db::[<__ $db _schema>]::*;

View File

@@ -46,7 +46,7 @@ impl Attachment {
pub fn get_url(&self, host: &str) -> String {
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
format!("{}/attachments/{}/{}?token={}", host, self.cipher_uuid, self.id, token)
format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id)
}
pub fn to_json(&self, host: &str) -> Value {
@@ -117,7 +117,7 @@ impl Attachment {
// upstream caller has already cleaned up the file as part of
// its own error handling.
Err(e) if e.kind() == ErrorKind::NotFound => {
debug!("File '{}' already deleted.", file_path);
debug!("File '{file_path}' already deleted.");
Ok(())
}
Err(e) => Err(e.into()),

View File

@@ -16,7 +16,7 @@ db_object! {
pub organization_uuid: Option<OrganizationId>,
pub request_device_identifier: DeviceId,
pub device_type: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
pub device_type: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub request_ip: String,
pub response_device_id: Option<DeviceId>,

View File

@@ -85,7 +85,7 @@ impl Cipher {
let mut validation_errors = serde_json::Map::new();
let max_note_size = CONFIG._max_note_size();
let max_note_size_msg =
format!("The field Notes exceeds the maximum encrypted value length of {} characters.", &max_note_size);
format!("The field Notes exceeds the maximum encrypted value length of {max_note_size} characters.");
for (index, cipher) in cipher_data.iter().enumerate() {
// Validate the note size and if it is exceeded return a warning
if let Some(note) = &cipher.notes {
@@ -318,7 +318,7 @@ impl Cipher {
// supports the "cipherDetails" type, though it seems like the
// Bitwarden clients will ignore extra fields.
//
// Ref: https://github.com/bitwarden/server/blob/master/src/Core/Models/Api/Response/CipherResponseModel.cs
// Ref: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Vault/Models/Response/CipherResponseModel.cs#L14
let mut json_object = json!({
"object": "cipherDetails",
"id": self.uuid,

View File

@@ -11,6 +11,7 @@ use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = collections)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Collection {
pub uuid: CollectionId,

View File

@@ -3,8 +3,12 @@ use derive_more::{Display, From};
use serde_json::Value;
use super::{AuthRequest, UserId};
use crate::{crypto, util::format_date, CONFIG};
use macros::IdFromParam;
use crate::{
crypto,
util::{format_date, get_uuid},
CONFIG,
};
use macros::{IdFromParam, UuidFromParam};
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -19,8 +23,8 @@ db_object! {
pub user_uuid: UserId,
pub name: String,
pub atype: i32, // https://github.com/bitwarden/server/blob/dcc199bcce4aa2d5621f6fab80f1b49d8b143418/src/Core/Enums/DeviceType.cs
pub push_uuid: Option<String>,
pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub push_uuid: Option<PushId>,
pub push_token: Option<String>,
pub refresh_token: String,
@@ -42,7 +46,7 @@ impl Device {
name,
atype,
push_uuid: None,
push_uuid: Some(PushId(get_uuid())),
push_token: None,
refresh_token: String::new(),
twofactor_remember: None,
@@ -54,7 +58,7 @@ impl Device {
"id": self.uuid,
"name": self.name,
"type": self.atype,
"identifier": self.push_uuid,
"identifier": self.uuid,
"creationDate": format_date(&self.created_at),
"isTrusted": false,
"object":"device"
@@ -73,7 +77,12 @@ impl Device {
self.twofactor_remember = None;
}
pub fn refresh_tokens(&mut self, user: &super::User, scope: Vec<String>) -> (String, i64) {
pub fn refresh_tokens(
&mut self,
user: &super::User,
scope: Vec<String>,
client_id: Option<String>,
) -> (String, i64) {
// If there is no refresh token, we create one
if self.refresh_token.is_empty() {
use data_encoding::BASE64URL;
@@ -84,6 +93,11 @@ impl Device {
let time_now = Utc::now();
self.updated_at = time_now.naive_utc();
// Generate a random push_uuid so if it doesn't already have one
if self.push_uuid.is_none() {
self.push_uuid = Some(PushId(get_uuid()));
}
// ---
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
@@ -121,6 +135,8 @@ impl Device {
// orgmanager,
sstamp: user.security_stamp.clone(),
device: self.uuid.clone(),
devicetype: DeviceType::from_i32(self.atype).to_string(),
client_id: client_id.unwrap_or("undefined".to_string()),
scope,
amr: vec!["Application".into()],
};
@@ -132,8 +148,8 @@ impl Device {
matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios)
}
pub fn is_registered(&self) -> bool {
self.push_uuid.is_some()
pub fn is_cli(&self) -> bool {
matches!(DeviceType::from_i32(self.atype), DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI)
}
}
@@ -152,10 +168,12 @@ impl DeviceWithAuthRequest {
"id": self.device.uuid,
"name": self.device.name,
"type": self.device.atype,
"identifier": self.device.push_uuid,
"identifier": self.device.uuid,
"creationDate": format_date(&self.device.created_at),
"devicePendingAuthRequest": auth_request,
"isTrusted": false,
"encryptedPublicKey": null,
"encryptedUserKey": null,
"object": "device",
})
}
@@ -391,3 +409,6 @@ impl DeviceType {
Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
)]
pub struct DeviceId(String);
#[derive(Clone, Debug, DieselNewType, Display, From, FromForm, Serialize, Deserialize, UuidFromParam)]
pub struct PushId(pub String);

View File

@@ -78,6 +78,7 @@ impl EmergencyAccess {
"grantorId": grantor_user.uuid,
"email": grantor_user.email,
"name": grantor_user.name,
"avatarColor": grantor_user.avatar_color,
"object": "emergencyAccessGrantorDetails",
})
}
@@ -106,6 +107,7 @@ impl EmergencyAccess {
"granteeId": grantee_user.uuid,
"email": grantee_user.email,
"name": grantee_user.name,
"avatarColor": grantee_user.avatar_color,
"object": "emergencyAccessGranteeDetails",
}))
}

View File

@@ -8,11 +8,12 @@ use crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG};
// https://bitwarden.com/help/event-logs/
db_object! {
// Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
// Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Api/Models/Public/Response/EventResponseModel.cs
// Upstream SQL: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Sql/dbo/Tables/Event.sql
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs
// Upstream SQL: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Sql/dbo/Tables/Event.sql
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = event)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Event {
pub uuid: EventId,
@@ -24,7 +25,7 @@ db_object! {
pub group_uuid: Option<GroupId>,
pub org_user_uuid: Option<MembershipId>,
pub act_user_uuid: Option<UserId>,
// Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs
// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub device_type: Option<i32>,
pub ip_address: Option<String>,
pub event_date: NaiveDateTime,
@@ -35,7 +36,7 @@ db_object! {
}
}
// Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/EventType.cs
// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/EventType.cs
#[derive(Debug, Copy, Clone)]
pub enum EventType {
// User
@@ -71,7 +72,6 @@ pub enum EventType {
CipherSoftDeleted = 1115,
CipherRestored = 1116,
CipherClientToggledCardNumberVisible = 1117,
CipherClientToggledTOTPSeedVisible = 1118,
// Collection
CollectionCreated = 1300,
@@ -87,7 +87,7 @@ pub enum EventType {
OrganizationUserInvited = 1500,
OrganizationUserConfirmed = 1501,
OrganizationUserUpdated = 1502,
OrganizationUserRemoved = 1503,
OrganizationUserRemoved = 1503, // Organization user data was deleted
OrganizationUserUpdatedGroups = 1504,
// OrganizationUserUnlinkedSso = 1505, // Not supported
OrganizationUserResetPasswordEnroll = 1506,
@@ -99,8 +99,8 @@ pub enum EventType {
OrganizationUserRestored = 1512,
OrganizationUserApprovedAuthRequest = 1513,
OrganizationUserRejectedAuthRequest = 1514,
OrganizationUserDeleted = 1515,
OrganizationUserLeft = 1516,
OrganizationUserDeleted = 1515, // Both user and organization user data were deleted
OrganizationUserLeft = 1516, // User voluntarily left the organization
// Organization
OrganizationUpdated = 1600,
@@ -187,7 +187,7 @@ impl Event {
}
/// Database methods
/// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
/// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
impl Event {
pub const PAGE_SIZE: i64 = 30;

View File

@@ -10,6 +10,7 @@ use serde_json::Value;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = groups)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Group {
pub uuid: GroupId,
@@ -67,16 +68,11 @@ impl Group {
}
pub fn to_json(&self) -> Value {
use crate::util::format_date;
json!({
"id": self.uuid,
"organizationId": self.organizations_uuid,
"name": self.name,
"accessAll": self.access_all,
"externalId": self.external_id,
"creationDate": format_date(&self.creation_date),
"revisionDate": format_date(&self.revision_date),
"object": "group"
})
}
@@ -297,7 +293,7 @@ impl Group {
pub async fn update_revision(uuid: &GroupId, conn: &mut DbConn) {
if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
warn!("Failed to update revision for {}: {:#?}", uuid, e);
warn!("Failed to update revision for {uuid}: {e:#?}");
}
}

View File

@@ -20,7 +20,7 @@ pub use self::attachment::{Attachment, AttachmentId};
pub use self::auth_request::{AuthRequest, AuthRequestId};
pub use self::cipher::{Cipher, CipherId, RepromptType};
pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};
pub use self::device::{Device, DeviceId, DeviceType};
pub use self::device::{Device, DeviceId, DeviceType, PushId};
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType};
pub use self::event::{Event, EventType};
pub use self::favorite::Favorite;

View File

@@ -21,7 +21,7 @@ db_object! {
}
}
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/PolicyType.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/PolicyType.cs
#[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)]
pub enum OrgPolicyType {
TwoFactorAuthentication = 0,
@@ -35,9 +35,13 @@ pub enum OrgPolicyType {
ResetPassword = 8,
// MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)
// DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed)
// ActivateAutofill = 11,
// AutomaticAppLogIn = 12,
// FreeFamiliesSponsorshipPolicy = 13,
RemoveUnlockWithPin = 14,
}
// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendOptionsPolicyData {
@@ -45,7 +49,7 @@ pub struct SendOptionsPolicyData {
pub disable_hide_email: bool,
}
// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResetPasswordDataModel {
@@ -79,14 +83,24 @@ impl OrgPolicy {
pub fn to_json(&self) -> Value {
let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
json!({
let mut policy = json!({
"id": self.uuid,
"organizationId": self.org_uuid,
"type": self.atype,
"data": data_json,
"enabled": self.enabled,
"object": "policy",
})
});
// Upstream adds this key/value
// Allow enabling Single Org policy when the organization has claimed domains.
// See: (https://github.com/bitwarden/server/pull/5565)
// We return the same to prevent possible issues
if self.atype == 8i32 {
policy["canToggleState"] = json!(true);
}
policy
}
}

View File

@@ -17,6 +17,7 @@ use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = organizations)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Organization {
pub uuid: OrganizationId,
@@ -28,6 +29,7 @@ db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = users_organizations)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Membership {
pub uuid: MembershipId,
@@ -54,7 +56,7 @@ db_object! {
}
}
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/OrganizationUserStatusType.cs
#[derive(PartialEq)]
pub enum MembershipStatus {
Revoked = -1,
@@ -175,7 +177,7 @@ impl Organization {
public_key,
}
}
// https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs
pub fn to_json(&self) -> Value {
json!({
"id": self.uuid,
@@ -201,7 +203,6 @@ impl Organization {
"useResetPassword": CONFIG.mail_enabled(),
"allowAdminAccessToAllCollectionItems": true,
"limitCollectionCreation": true,
"limitCollectionCreationDeletion": true,
"limitCollectionDeletion": true,
"businessName": self.name,
@@ -422,7 +423,7 @@ impl Membership {
"manageScim": false // Not supported (Not AGPLv3 Licensed)
});
// https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/ProfileOrganizationResponseModel.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs
json!({
"id": self.org_uuid,
"identifier": null, // Not supported
@@ -449,6 +450,8 @@ impl Membership {
"usePasswordManager": true,
"useCustomPermissions": true,
"useActivateAutofillPolicy": false,
"useAdminSponsoredFamilies": false,
"useRiskInsights": false, // Not supported (Not AGPLv3 Licensed)
"organizationUserId": self.uuid,
"providerId": null,
@@ -456,7 +459,6 @@ impl Membership {
"providerType": null,
"familySponsorshipFriendlyName": null,
"familySponsorshipAvailable": false,
"planProductType": 3,
"productTierType": 3, // Enterprise tier
"keyConnectorEnabled": false,
"keyConnectorUrl": null,
@@ -465,10 +467,11 @@ impl Membership {
"familySponsorshipToDelete": null,
"accessSecretsManager": false,
"limitCollectionCreation": self.atype < MembershipType::Manager, // If less then a manager return true, to limit collection creations
"limitCollectionCreationDeletion": true,
"limitCollectionDeletion": true,
"limitItemDeletion": false,
"allowAdminAccessToAllCollectionItems": true,
"userIsManagedByOrganization": false, // Means not managed via the Members UI, like SSO
"userIsClaimedByOrganization": false, // The new key instead of the obsolete userIsManagedByOrganization
"permissions": permissions,
@@ -517,7 +520,7 @@ impl Membership {
CONFIG.org_groups_enabled() && Group::is_in_full_access_group(&self.user_uuid, &self.org_uuid, conn).await;
// If collections are to be included, only include them if the user does not have full access via a group or defined to the user it self
let collections: Vec<Value> = if include_collections && !(full_access_group || self.has_full_access()) {
let collections: Vec<Value> = if include_collections && !(full_access_group || self.access_all) {
// Get all collections for the user here already to prevent more queries
let cu: HashMap<CollectionId, CollectionUser> =
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
@@ -614,6 +617,8 @@ impl Membership {
"permissions": permissions,
"ssoBound": false, // Not supported
"managedByOrganization": false, // This key is obsolete replaced by claimedByOrganization
"claimedByOrganization": false, // Means not managed via the Members UI, like SSO
"usesKeyConnector": false, // Not supported
"accessSecretsManager": false, // Not supported (Not AGPLv3 Licensed)
@@ -861,6 +866,21 @@ impl Membership {
}}
}
// Get all users which are either owner or admin, or a manager which can manage/access all
pub async fn find_confirmed_and_manage_all_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32])
.or(users_organizations::atype.eq(MembershipType::Manager as i32).and(users_organizations::access_all.eq(true)))
)
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
}}
}
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
users_organizations::table

View File

@@ -173,8 +173,8 @@ impl User {
/// * `password` - A str which contains a hashed version of the users master password.
/// * `new_key` - A String which contains the new aKey value of the users master password.
/// * `allow_next_route` - A Option<Vec<String>> with the function names of the next allowed (rocket) routes.
/// These routes are able to use the previous stamp id for the next 2 minutes.
/// After these 2 minutes this stamp will expire.
/// These routes are able to use the previous stamp id for the next 2 minutes.
/// After these 2 minutes this stamp will expire.
///
pub fn set_password(
&mut self,
@@ -206,8 +206,8 @@ impl User {
///
/// # Arguments
/// * `route_exception` - A Vec<String> with the function names of the next allowed (rocket) routes.
/// These routes are able to use the previous stamp id for the next 2 minutes.
/// After these 2 minutes this stamp will expire.
/// These routes are able to use the previous stamp id for the next 2 minutes.
/// After these 2 minutes this stamp will expire.
///
pub fn set_stamp_exception(&mut self, route_exception: Vec<String>) {
let stamp_exception = UserStampException {
@@ -249,7 +249,6 @@ impl User {
"emailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(),
"premium": true,
"premiumFromOrganization": false,
"masterPasswordHint": self.password_hint,
"culture": "en-US",
"twoFactorEnabled": twofactor_enabled,
"key": self.akey,
@@ -334,7 +333,7 @@ impl User {
pub async fn update_uuid_revision(uuid: &UserId, conn: &mut DbConn) {
if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
warn!("Failed to update revision for {}: {:#?}", uuid, e);
warn!("Failed to update revision for {uuid}: {e:#?}");
}
}

View File

@@ -59,6 +59,8 @@ use yubico::yubicoerror::YubicoError as YubiErr;
#[derive(Serialize)]
pub struct Empty {}
pub struct Compact {}
// Error struct
// Contains a String error message, meant for the user and an enum variant, with an error of different types.
//
@@ -69,6 +71,7 @@ make_error! {
Empty(Empty): _no_source, _serialize,
// Used to represent err! calls
Simple(String): _no_source, _api_error,
Compact(Compact): _no_source, _api_error_small,
// Used in our custom http client to handle non-global IPs and blocked domains
CustomHttpClient(CustomHttpClientError): _has_source, _api_error,
@@ -132,6 +135,12 @@ impl Error {
self
}
#[must_use]
pub fn with_kind(mut self, kind: ErrorKind) -> Self {
self.error = kind;
self
}
#[must_use]
pub const fn with_code(mut self, code: u16) -> Self {
self.error_code = code;
@@ -200,6 +209,18 @@ fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
_serialize(&json, "")
}
fn _api_error_small(_: &impl std::any::Any, msg: &str) -> String {
let json = json!({
"message": msg,
"validationErrors": null,
"exceptionMessage": null,
"exceptionStackTrace": null,
"innerExceptionMessage": null,
"object": "error"
});
_serialize(&json, "")
}
//
// Rocket responder impl
//
@@ -212,9 +233,8 @@ use rocket::response::{self, Responder, Response};
impl Responder<'_, 'static> for Error {
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
match self.error {
ErrorKind::Empty(_) => {} // Don't print the error in this situation
ErrorKind::Simple(_) => {} // Don't print the error in this situation
_ => error!(target: "error", "{:#?}", self),
ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Compact(_) => {} // Don't print the error in this situation
_ => error!(target: "error", "{self:#?}"),
};
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
@@ -228,6 +248,10 @@ impl Responder<'_, 'static> for Error {
//
#[macro_export]
macro_rules! err {
($kind:ident, $msg:expr) => {{
error!("{}", $msg);
return Err($crate::error::Error::new($msg, $msg).with_kind($crate::error::ErrorKind::$kind($crate::error::$kind {})));
}};
($msg:expr) => {{
error!("{}", $msg);
return Err($crate::error::Error::new($msg, $msg));

View File

@@ -6,7 +6,7 @@ use std::{
time::Duration,
};
use hickory_resolver::{system_conf::read_system_conf, TokioAsyncResolver};
use hickory_resolver::{name_server::TokioConnectionProvider, TokioResolver};
use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::{
@@ -173,7 +173,7 @@ impl std::error::Error for CustomHttpClientError {}
#[derive(Debug, Clone)]
enum CustomDnsResolver {
Default(),
Hickory(Arc<TokioAsyncResolver>),
Hickory(Arc<TokioResolver>),
}
type BoxError = Box<dyn std::error::Error + Send + Sync>;
@@ -184,9 +184,9 @@ impl CustomDnsResolver {
}
fn new() -> Arc<Self> {
match read_system_conf() {
Ok((config, opts)) => {
let resolver = TokioAsyncResolver::tokio(config.clone(), opts.clone());
match TokioResolver::builder(TokioConnectionProvider::default()) {
Ok(builder) => {
let resolver = builder.build();
Arc::new(Self::Hickory(Arc::new(resolver)))
}
Err(e) => {

View File

@@ -85,7 +85,7 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
smtp_client.authentication(selected_mechanisms)
} else {
// Only show a warning, and return without setting an actual authentication mechanism
warn!("No valid SMTP Auth mechanism found for '{}', using default values", mechanism);
warn!("No valid SMTP Auth mechanism found for '{mechanism}', using default values");
smtp_client
}
}
@@ -201,6 +201,27 @@ pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult {
send_email(address, &subject, body_html, body_text).await
}
pub async fn send_register_verify_email(email: &str, token: &str) -> EmptyResult {
let mut query = url::Url::parse("https://query.builder").unwrap();
query.query_pairs_mut().append_pair("email", email).append_pair("token", token);
let query_string = match query.query() {
None => err!("Failed to build verify URL query parameters"),
Some(query) => query,
};
let (subject, body_html, body_text) = get_text(
"email/register_verify_email",
json!({
// `url.Url` would place the anchor `#` after the query parameters
"url": format!("{}/#/finish-signup/?{query_string}", CONFIG.domain()),
"img_src": CONFIG._smtp_img_src(),
"email": email,
}),
)?;
send_email(email, &subject, body_html, body_text).await
}
pub async fn send_welcome(address: &str) -> EmptyResult {
let (subject, body_html, body_text) = get_text(
"email/welcome",
@@ -293,7 +314,7 @@ pub async fn send_invite(
"email/send_org_invite",
json!({
// `url.Url` would place the anchor `#` after the query parameters
"url": format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string),
"url": format!("{}/#/accept-organization/?{query_string}", CONFIG.domain()),
"img_src": CONFIG._smtp_img_src(),
"org_name": org_name,
}),
@@ -549,6 +570,20 @@ pub async fn send_change_email(address: &str, token: &str) -> EmptyResult {
send_email(address, &subject, body_html, body_text).await
}
pub async fn send_change_email_existing(address: &str, acting_address: &str) -> EmptyResult {
let (subject, body_html, body_text) = get_text(
"email/change_email_existing",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"existing_address": address,
"acting_address": acting_address,
}),
)?;
send_email(address, &subject, body_html, body_text).await
}
pub async fn send_test(address: &str) -> EmptyResult {
let (subject, body_html, body_text) = get_text(
"email/smtp_test",
@@ -594,13 +629,13 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
// Match some common errors and make them more user friendly
Err(e) => {
if e.is_client() {
debug!("Sendmail client error: {:?}", e);
debug!("Sendmail client error: {e:?}");
err!(format!("Sendmail client error: {e}"));
} else if e.is_response() {
debug!("Sendmail response error: {:?}", e);
debug!("Sendmail response error: {e:?}");
err!(format!("Sendmail response error: {e}"));
} else {
debug!("Sendmail error: {:?}", e);
debug!("Sendmail error: {e:?}");
err!(format!("Sendmail error: {e}"));
}
}
@@ -611,13 +646,13 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
// Match some common errors and make them more user friendly
Err(e) => {
if e.is_client() {
debug!("SMTP client error: {:#?}", e);
debug!("SMTP client error: {e:#?}");
err!(format!("SMTP client error: {e}"));
} else if e.is_transient() {
debug!("SMTP 4xx error: {:#?}", e);
debug!("SMTP 4xx error: {e:#?}");
err!(format!("SMTP 4xx error: {e}"));
} else if e.is_permanent() {
debug!("SMTP 5xx error: {:#?}", e);
debug!("SMTP 5xx error: {e:#?}");
let mut msg = e.to_string();
// Add a special check for 535 to add a more descriptive message
if msg.contains("(535)") {
@@ -625,13 +660,13 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
}
err!(format!("SMTP 5xx error: {msg}"));
} else if e.is_timeout() {
debug!("SMTP timeout error: {:#?}", e);
debug!("SMTP timeout error: {e:#?}");
err!(format!("SMTP timeout error: {e}"));
} else if e.is_tls() {
debug!("SMTP encryption error: {:#?}", e);
debug!("SMTP encryption error: {e:#?}");
err!(format!("SMTP encryption error: {e}"));
} else {
debug!("SMTP error: {:#?}", e);
debug!("SMTP error: {e:#?}");
err!(format!("SMTP error: {e}"));
}
}

View File

@@ -430,10 +430,7 @@ fn init_logging() -> Result<log::LevelFilter, Error> {
}
None => error!(
target: "panic",
"thread '{}' panicked at '{}'\n{:}",
thread,
msg,
backtrace
"thread '{thread}' panicked at '{msg}'\n{backtrace:}"
),
}
}));
@@ -453,7 +450,7 @@ fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
match syslog::unix(syslog_fmt) {
Ok(sl) => logger.chain(sl),
Err(e) => {
error!("Unable to connect to syslog: {:?}", e);
error!("Unable to connect to syslog: {e:?}");
logger
}
}
@@ -469,7 +466,7 @@ async fn check_data_folder() {
let data_folder = &CONFIG.data_folder();
let path = Path::new(data_folder);
if !path.exists() {
error!("Data folder '{}' doesn't exist.", data_folder);
error!("Data folder '{data_folder}' doesn't exist.");
if is_running_in_container() {
error!("Verify that your data volume is mounted at the correct location.");
} else {
@@ -478,7 +475,7 @@ async fn check_data_folder() {
exit(1);
}
if !path.is_dir() {
error!("Data folder '{}' is not a directory.", data_folder);
error!("Data folder '{data_folder}' is not a directory.");
exit(1);
}
@@ -552,7 +549,7 @@ async fn create_db_pool() -> db::DbPool {
match util::retry_db(db::DbPool::from_config, CONFIG.db_connection_retries()).await {
Ok(p) => p,
Err(e) => {
error!("Error creating database pool: {:?}", e);
error!("Error creating database pool: {e:?}");
exit(1);
}
}

View File

@@ -38,8 +38,8 @@ img {
max-width: 130px;
}
#users-table .vw-actions, #orgs-table .vw-actions {
min-width: 135px;
max-width: 140px;
min-width: 155px;
max-width: 160px;
}
#users-table .vw-org-cell {
max-height: 120px;

View File

@@ -29,7 +29,7 @@ function isValidIp(ip) {
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}
function checkVersions(platform, installed, latest, commit=null) {
function checkVersions(platform, installed, latest, commit=null, pre_release=false) {
if (installed === "-" || latest === "-") {
document.getElementById(`${platform}-failed`).classList.remove("d-none");
return;
@@ -37,10 +37,12 @@ function checkVersions(platform, installed, latest, commit=null) {
// Only check basic versions, no commit revisions
if (commit === null || installed.indexOf("-") === -1) {
if (installed !== latest) {
document.getElementById(`${platform}-warning`).classList.remove("d-none");
} else {
if (platform === "web" && pre_release === true) {
document.getElementById(`${platform}-prerelease`).classList.remove("d-none");
} else if (installed == latest) {
document.getElementById(`${platform}-success`).classList.remove("d-none");
} else {
document.getElementById(`${platform}-warning`).classList.remove("d-none");
}
} else {
// Check if this is a branched version.
@@ -86,7 +88,7 @@ async function generateSupportString(event, dj) {
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
supportString += `* Database type: ${dj.db_type}\n`;
supportString += `* Database version: ${dj.db_version}\n`;
supportString += `* Environment settings overridden!: ${dj.overrides !== ""}\n`;
supportString += `* Uses config.json: ${dj.overrides !== ""}\n`;
supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`;
if (dj.ip_header_exists) {
supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`;
@@ -94,6 +96,9 @@ async function generateSupportString(event, dj) {
supportString += `* Internet access: ${dj.has_http_access}\n`;
supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`;
supportString += `* DNS Check: ${dnsCheck}\n`;
if (dj.tz_env !== "") {
supportString += `* TZ environment: ${dj.tz_env}\n`;
}
supportString += `* Browser/Server Time Check: ${timeCheck}\n`;
supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`;
supportString += `* Domain Configuration Check: ${domainCheck}\n`;
@@ -206,7 +211,7 @@ function initVersionCheck(dj) {
if (!dj.running_within_container) {
const webInstalled = dj.web_vault_version;
const webLatest = dj.latest_web_build;
checkVersions("web", webInstalled, webLatest);
checkVersions("web", webInstalled, webLatest, null, dj.web_vault_pre_release);
}
}

View File

@@ -1,6 +1,6 @@
/*!
* Bootstrap v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Bootstrap v5.3.6 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
(function (global, factory) {
@@ -205,7 +205,7 @@
* @param {HTMLElement} element
* @return void
*
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
* @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
*/
const reflow = element => {
element.offsetHeight; // eslint-disable-line no-unused-expressions
@@ -250,7 +250,7 @@
});
};
const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {
return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;
return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue;
};
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
if (!waitForTransition) {
@@ -572,7 +572,7 @@
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));
for (const key of bsKeys) {
let pureKey = key.replace(/^bs/, '');
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1);
attributes[pureKey] = normalizeData(element.dataset[key]);
}
return attributes;
@@ -647,7 +647,7 @@
* Constants
*/
const VERSION = '5.3.3';
const VERSION = '5.3.6';
/**
* Class definition
@@ -673,6 +673,8 @@
this[propertyName] = null;
}
}
// Private
_queueCallback(callback, element, isAnimated = true) {
executeAfterTransition(callback, element, isAnimated);
}
@@ -1604,11 +1606,11 @@
this._element.style[dimension] = '';
this._queueCallback(complete, this._element, true);
}
// Private
_isShown(element = this._element) {
return element.classList.contains(CLASS_NAME_SHOW$7);
}
// Private
_configAfterMerge(config) {
config.toggle = Boolean(config.toggle); // Coerce string values
config.parent = getElement(config.parent);
@@ -2666,7 +2668,6 @@
var popperOffsets = computeOffsets({
reference: referenceClientRect,
element: popperRect,
strategy: 'absolute',
placement: placement
});
var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));
@@ -2994,7 +2995,6 @@
state.modifiersData[name] = computeOffsets({
reference: state.rects.reference,
element: state.rects.popper,
strategy: 'absolute',
placement: state.placement
});
} // eslint-disable-next-line import/no-unused-modules
@@ -3690,6 +3690,9 @@
this._element.setAttribute('aria-expanded', 'false');
Manipulator.removeDataAttribute(this._menu, 'popper');
EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);
// Explicitly return focus to the trigger element
this._element.focus();
}
_getConfig(config) {
config = super._getConfig(config);
@@ -3701,7 +3704,7 @@
}
_createPopper() {
if (typeof Popper === 'undefined') {
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)');
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)');
}
let referenceElement = this._element;
if (this._config.reference === 'parent') {
@@ -3780,7 +3783,7 @@
}
return {
...defaultBsPopperConfig,
...execute(this._config.popperConfig, [defaultBsPopperConfig])
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
};
}
_selectMenuItem({
@@ -4967,7 +4970,7 @@
return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;
}
_resolvePossibleFunction(arg) {
return execute(arg, [this]);
return execute(arg, [undefined, this]);
}
_putElementInTemplate(element, templateElement) {
if (this._config.html) {
@@ -5066,7 +5069,7 @@
class Tooltip extends BaseComponent {
constructor(element, config) {
if (typeof Popper === 'undefined') {
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)');
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)');
}
super(element, config);
@@ -5112,7 +5115,6 @@
if (!this._isEnabled) {
return;
}
this._activeTrigger.click = !this._activeTrigger.click;
if (this._isShown()) {
this._leave();
return;
@@ -5300,7 +5302,7 @@
return offset;
}
_resolvePossibleFunction(arg) {
return execute(arg, [this._element]);
return execute(arg, [this._element, this._element]);
}
_getPopperConfig(attachment) {
const defaultBsPopperConfig = {
@@ -5338,7 +5340,7 @@
};
return {
...defaultBsPopperConfig,
...execute(this._config.popperConfig, [defaultBsPopperConfig])
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
};
}
_setListeners() {
@@ -6212,7 +6214,6 @@
}
// Private
_maybeScheduleHide() {
if (!this._config.autohide) {
return;

View File

@@ -1,7 +1,7 @@
@charset "UTF-8";
/*!
* Bootstrap v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Bootstrap v5.3.6 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
@@ -517,8 +517,8 @@ legend {
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
legend {
@@ -601,9 +601,9 @@ progress {
}
.display-1 {
font-size: calc(1.625rem + 4.5vw);
font-weight: 300;
line-height: 1.2;
font-size: calc(1.625rem + 4.5vw);
}
@media (min-width: 1200px) {
.display-1 {
@@ -612,9 +612,9 @@ progress {
}
.display-2 {
font-size: calc(1.575rem + 3.9vw);
font-weight: 300;
line-height: 1.2;
font-size: calc(1.575rem + 3.9vw);
}
@media (min-width: 1200px) {
.display-2 {
@@ -623,9 +623,9 @@ progress {
}
.display-3 {
font-size: calc(1.525rem + 3.3vw);
font-weight: 300;
line-height: 1.2;
font-size: calc(1.525rem + 3.3vw);
}
@media (min-width: 1200px) {
.display-3 {
@@ -634,9 +634,9 @@ progress {
}
.display-4 {
font-size: calc(1.475rem + 2.7vw);
font-weight: 300;
line-height: 1.2;
font-size: calc(1.475rem + 2.7vw);
}
@media (min-width: 1200px) {
.display-4 {
@@ -645,9 +645,9 @@ progress {
}
.display-5 {
font-size: calc(1.425rem + 2.1vw);
font-weight: 300;
line-height: 1.2;
font-size: calc(1.425rem + 2.1vw);
}
@media (min-width: 1200px) {
.display-5 {
@@ -656,9 +656,9 @@ progress {
}
.display-6 {
font-size: calc(1.375rem + 1.5vw);
font-weight: 300;
line-height: 1.2;
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
.display-6 {
@@ -803,7 +803,7 @@ progress {
}
.col {
flex: 1 0 0%;
flex: 1 0 0;
}
.row-cols-auto > * {
@@ -1012,7 +1012,7 @@ progress {
@media (min-width: 576px) {
.col-sm {
flex: 1 0 0%;
flex: 1 0 0;
}
.row-cols-sm-auto > * {
flex: 0 0 auto;
@@ -1181,7 +1181,7 @@ progress {
}
@media (min-width: 768px) {
.col-md {
flex: 1 0 0%;
flex: 1 0 0;
}
.row-cols-md-auto > * {
flex: 0 0 auto;
@@ -1350,7 +1350,7 @@ progress {
}
@media (min-width: 992px) {
.col-lg {
flex: 1 0 0%;
flex: 1 0 0;
}
.row-cols-lg-auto > * {
flex: 0 0 auto;
@@ -1519,7 +1519,7 @@ progress {
}
@media (min-width: 1200px) {
.col-xl {
flex: 1 0 0%;
flex: 1 0 0;
}
.row-cols-xl-auto > * {
flex: 0 0 auto;
@@ -1688,7 +1688,7 @@ progress {
}
@media (min-width: 1400px) {
.col-xxl {
flex: 1 0 0%;
flex: 1 0 0;
}
.row-cols-xxl-auto > * {
flex: 0 0 auto;
@@ -2156,10 +2156,6 @@ progress {
display: block;
padding: 0;
}
.form-control::-moz-placeholder {
color: var(--bs-secondary-color);
opacity: 1;
}
.form-control::placeholder {
color: var(--bs-secondary-color);
opacity: 1;
@@ -2607,9 +2603,11 @@ textarea.form-control-lg {
top: 0;
left: 0;
z-index: 2;
max-width: 100%;
height: 100%;
padding: 1rem 0.75rem;
overflow: hidden;
color: rgba(var(--bs-body-color-rgb), 0.65);
text-align: start;
text-overflow: ellipsis;
white-space: nowrap;
@@ -2627,17 +2625,10 @@ textarea.form-control-lg {
.form-floating > .form-control-plaintext {
padding: 1rem 0.75rem;
}
.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder {
color: transparent;
}
.form-floating > .form-control::placeholder,
.form-floating > .form-control-plaintext::placeholder {
color: transparent;
}
.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) {
padding-top: 1.625rem;
padding-bottom: 0.625rem;
}
.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),
.form-floating > .form-control-plaintext:focus,
.form-floating > .form-control-plaintext:not(:placeholder-shown) {
@@ -2652,43 +2643,30 @@ textarea.form-control-lg {
.form-floating > .form-select {
padding-top: 1.625rem;
padding-bottom: 0.625rem;
}
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label {
color: rgba(var(--bs-body-color-rgb), 0.65);
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
padding-left: 0.75rem;
}
.form-floating > .form-control:focus ~ label,
.form-floating > .form-control:not(:placeholder-shown) ~ label,
.form-floating > .form-control-plaintext ~ label,
.form-floating > .form-select ~ label {
color: rgba(var(--bs-body-color-rgb), 0.65);
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label::after {
position: absolute;
inset: 1rem 0.375rem;
z-index: -1;
height: 1.5em;
content: "";
background-color: var(--bs-body-bg);
border-radius: var(--bs-border-radius);
}
.form-floating > .form-control:focus ~ label::after,
.form-floating > .form-control:not(:placeholder-shown) ~ label::after,
.form-floating > .form-control-plaintext ~ label::after,
.form-floating > .form-select ~ label::after {
position: absolute;
inset: 1rem 0.375rem;
z-index: -1;
height: 1.5em;
content: "";
background-color: var(--bs-body-bg);
border-radius: var(--bs-border-radius);
}
.form-floating > .form-control:-webkit-autofill ~ label {
color: rgba(var(--bs-body-color-rgb), 0.65);
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}
.form-floating > textarea:focus ~ label::after,
.form-floating > textarea:not(:placeholder-shown) ~ label::after {
position: absolute;
inset: 1rem 0.375rem;
z-index: -1;
height: 1.5em;
content: "";
background-color: var(--bs-body-bg);
border-radius: var(--bs-border-radius);
}
.form-floating > textarea:disabled ~ label::after {
background-color: var(--bs-secondary-bg);
}
.form-floating > .form-control-plaintext ~ label {
border-width: var(--bs-border-width) 0;
}
@@ -2696,10 +2674,6 @@ textarea.form-control-lg {
.form-floating > .form-control:disabled ~ label {
color: #6c757d;
}
.form-floating > :disabled ~ label::after,
.form-floating > .form-control:disabled ~ label::after {
background-color: var(--bs-secondary-bg);
}
.input-group {
position: relative;
@@ -2782,7 +2756,7 @@ textarea.form-control-lg {
border-bottom-right-radius: 0;
}
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
margin-left: calc(var(--bs-border-width) * -1);
margin-left: calc(-1 * var(--bs-border-width));
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
@@ -2824,7 +2798,7 @@ textarea.form-control-lg {
.was-validated .form-control:valid, .form-control.is-valid {
border-color: var(--bs-form-valid-border-color);
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
@@ -2843,7 +2817,7 @@ textarea.form-control-lg {
border-color: var(--bs-form-valid-border-color);
}
.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] {
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");
padding-right: 4.125rem;
background-position: right 0.75rem center, center right 2.25rem;
background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
@@ -3755,7 +3729,7 @@ textarea.form-control-lg {
}
.btn-group > :not(.btn-check:first-child) + .btn,
.btn-group > .btn-group:not(:first-child) {
margin-left: calc(var(--bs-border-width) * -1);
margin-left: calc(-1 * var(--bs-border-width));
}
.btn-group > .btn:not(:last-child):not(.dropdown-toggle),
.btn-group > .btn.dropdown-toggle-split:first-child,
@@ -3802,14 +3776,15 @@ textarea.form-control-lg {
}
.btn-group-vertical > .btn:not(:first-child),
.btn-group-vertical > .btn-group:not(:first-child) {
margin-top: calc(var(--bs-border-width) * -1);
margin-top: calc(-1 * var(--bs-border-width));
}
.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),
.btn-group-vertical > .btn-group:not(:last-child) > .btn {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.btn-group-vertical > .btn ~ .btn,
.btn-group-vertical > .btn:nth-child(n+3),
.btn-group-vertical > :not(.btn-check) + .btn,
.btn-group-vertical > .btn-group:not(:first-child) > .btn {
border-top-left-radius: 0;
border-top-right-radius: 0;
@@ -3933,8 +3908,8 @@ textarea.form-control-lg {
.nav-justified > .nav-link,
.nav-justified .nav-item {
flex-basis: 0;
flex-grow: 1;
flex-basis: 0;
text-align: center;
}
@@ -4035,8 +4010,8 @@ textarea.form-control-lg {
}
.navbar-collapse {
flex-basis: 100%;
flex-grow: 1;
flex-basis: 100%;
align-items: center;
}
@@ -4531,7 +4506,7 @@ textarea.form-control-lg {
flex-flow: row wrap;
}
.card-group > .card {
flex: 1 0 0%;
flex: 1 0 0;
margin-bottom: 0;
}
.card-group > .card + .card {
@@ -4542,24 +4517,24 @@ textarea.form-control-lg {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.card-group > .card:not(:last-child) .card-img-top,
.card-group > .card:not(:last-child) .card-header {
.card-group > .card:not(:last-child) > .card-img-top,
.card-group > .card:not(:last-child) > .card-header {
border-top-right-radius: 0;
}
.card-group > .card:not(:last-child) .card-img-bottom,
.card-group > .card:not(:last-child) .card-footer {
.card-group > .card:not(:last-child) > .card-img-bottom,
.card-group > .card:not(:last-child) > .card-footer {
border-bottom-right-radius: 0;
}
.card-group > .card:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.card-group > .card:not(:first-child) .card-img-top,
.card-group > .card:not(:first-child) .card-header {
.card-group > .card:not(:first-child) > .card-img-top,
.card-group > .card:not(:first-child) > .card-header {
border-top-left-radius: 0;
}
.card-group > .card:not(:first-child) .card-img-bottom,
.card-group > .card:not(:first-child) .card-footer {
.card-group > .card:not(:first-child) > .card-img-bottom,
.card-group > .card:not(:first-child) > .card-footer {
border-bottom-left-radius: 0;
}
}
@@ -4576,11 +4551,11 @@ textarea.form-control-lg {
--bs-accordion-btn-padding-y: 1rem;
--bs-accordion-btn-color: var(--bs-body-color);
--bs-accordion-btn-bg: var(--bs-accordion-bg);
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
--bs-accordion-btn-icon-width: 1.25rem;
--bs-accordion-btn-icon-transform: rotate(-180deg);
--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
--bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
--bs-accordion-body-padding-x: 1.25rem;
--bs-accordion-body-padding-y: 1rem;
@@ -4690,16 +4665,15 @@ textarea.form-control-lg {
.accordion-flush > .accordion-item:last-child {
border-bottom: 0;
}
.accordion-flush > .accordion-item > .accordion-header .accordion-button, .accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {
border-radius: 0;
}
.accordion-flush > .accordion-item > .accordion-collapse {
.accordion-flush > .accordion-item > .accordion-collapse,
.accordion-flush > .accordion-item > .accordion-header .accordion-button,
.accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {
border-radius: 0;
}
[data-bs-theme=dark] .accordion-button::after {
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
}
.breadcrumb {
@@ -4803,7 +4777,7 @@ textarea.form-control-lg {
}
.page-item:not(:first-child) .page-link {
margin-left: calc(var(--bs-border-width) * -1);
margin-left: calc(-1 * var(--bs-border-width));
}
.page-item:first-child .page-link {
border-top-left-radius: var(--bs-pagination-border-radius);
@@ -4952,7 +4926,7 @@ textarea.form-control-lg {
@keyframes progress-bar-stripes {
0% {
background-position-x: 1rem;
background-position-x: var(--bs-progress-height);
}
}
.progress,
@@ -5046,22 +5020,6 @@ textarea.form-control-lg {
counter-increment: section;
}
.list-group-item-action {
width: 100%;
color: var(--bs-list-group-action-color);
text-align: inherit;
}
.list-group-item-action:hover, .list-group-item-action:focus {
z-index: 1;
color: var(--bs-list-group-action-hover-color);
text-decoration: none;
background-color: var(--bs-list-group-action-hover-bg);
}
.list-group-item-action:active {
color: var(--bs-list-group-action-active-color);
background-color: var(--bs-list-group-action-active-bg);
}
.list-group-item {
position: relative;
display: block;
@@ -5098,6 +5056,22 @@ textarea.form-control-lg {
border-top-width: var(--bs-list-group-border-width);
}
.list-group-item-action {
width: 100%;
color: var(--bs-list-group-action-color);
text-align: inherit;
}
.list-group-item-action:not(.active):hover, .list-group-item-action:not(.active):focus {
z-index: 1;
color: var(--bs-list-group-action-hover-color);
text-decoration: none;
background-color: var(--bs-list-group-action-hover-bg);
}
.list-group-item-action:not(.active):active {
color: var(--bs-list-group-action-active-color);
background-color: var(--bs-list-group-action-active-bg);
}
.list-group-horizontal {
flex-direction: row;
}
@@ -5357,19 +5331,19 @@ textarea.form-control-lg {
.btn-close {
--bs-btn-close-color: #000;
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");
--bs-btn-close-opacity: 0.5;
--bs-btn-close-hover-opacity: 0.75;
--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
--bs-btn-close-focus-opacity: 1;
--bs-btn-close-disabled-opacity: 0.25;
--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
box-sizing: content-box;
width: 1em;
height: 1em;
padding: 0.25em 0.25em;
color: var(--bs-btn-close-color);
background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;
filter: var(--bs-btn-close-filter);
border: 0;
border-radius: 0.375rem;
opacity: var(--bs-btn-close-opacity);
@@ -5393,11 +5367,16 @@ textarea.form-control-lg {
}
.btn-close-white {
filter: var(--bs-btn-close-white-filter);
--bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);
}
[data-bs-theme=dark] .btn-close {
filter: var(--bs-btn-close-white-filter);
:root,
[data-bs-theme=light] {
--bs-btn-close-filter: ;
}
[data-bs-theme=dark] {
--bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);
}
.toast {
@@ -5474,7 +5453,7 @@ textarea.form-control-lg {
--bs-modal-width: 500px;
--bs-modal-padding: 1rem;
--bs-modal-margin: 0.5rem;
--bs-modal-color: ;
--bs-modal-color: var(--bs-body-color);
--bs-modal-bg: var(--bs-body-bg);
--bs-modal-border-color: var(--bs-border-color-translucent);
--bs-modal-border-width: var(--bs-border-width);
@@ -5510,8 +5489,8 @@ textarea.form-control-lg {
pointer-events: none;
}
.modal.fade .modal-dialog {
transition: transform 0.3s ease-out;
transform: translate(0, -50px);
transition: transform 0.3s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.modal.fade .modal-dialog {
@@ -5586,7 +5565,10 @@ textarea.form-control-lg {
}
.modal-header .btn-close {
padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5);
margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto;
margin-top: calc(-0.5 * var(--bs-modal-header-padding-y));
margin-right: calc(-0.5 * var(--bs-modal-header-padding-x));
margin-bottom: calc(-0.5 * var(--bs-modal-header-padding-y));
margin-left: auto;
}
.modal-title {
@@ -6107,6 +6089,7 @@ textarea.form-control-lg {
color: #fff;
text-align: center;
background: none;
filter: var(--bs-carousel-control-icon-filter);
border: 0;
opacity: 0.5;
transition: opacity 0.15s ease;
@@ -6145,11 +6128,11 @@ textarea.form-control-lg {
}
.carousel-control-prev-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")*/;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")*/;
}
.carousel-control-next-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")*/;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")*/;
}
.carousel-indicators {
@@ -6175,7 +6158,7 @@ textarea.form-control-lg {
margin-left: 3px;
text-indent: -999px;
cursor: pointer;
background-color: #fff;
background-color: var(--bs-carousel-indicator-active-bg);
background-clip: padding-box;
border: 0;
border-top: 10px solid transparent;
@@ -6199,31 +6182,27 @@ textarea.form-control-lg {
left: 15%;
padding-top: 1.25rem;
padding-bottom: 1.25rem;
color: #fff;
color: var(--bs-carousel-caption-color);
text-align: center;
}
.carousel-dark .carousel-control-prev-icon,
.carousel-dark .carousel-control-next-icon {
filter: invert(1) grayscale(100);
}
.carousel-dark .carousel-indicators [data-bs-target] {
background-color: #000;
}
.carousel-dark .carousel-caption {
color: #000;
.carousel-dark {
--bs-carousel-indicator-active-bg: #000;
--bs-carousel-caption-color: #000;
--bs-carousel-control-icon-filter: invert(1) grayscale(100);
}
[data-bs-theme=dark] .carousel .carousel-control-prev-icon,
[data-bs-theme=dark] .carousel .carousel-control-next-icon, [data-bs-theme=dark].carousel .carousel-control-prev-icon,
[data-bs-theme=dark].carousel .carousel-control-next-icon {
filter: invert(1) grayscale(100);
:root,
[data-bs-theme=light] {
--bs-carousel-indicator-active-bg: #fff;
--bs-carousel-caption-color: #fff;
--bs-carousel-control-icon-filter: ;
}
[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=dark].carousel .carousel-indicators [data-bs-target] {
background-color: #000;
}
[data-bs-theme=dark] .carousel .carousel-caption, [data-bs-theme=dark].carousel .carousel-caption {
color: #000;
[data-bs-theme=dark] {
--bs-carousel-indicator-active-bg: #000;
--bs-carousel-caption-color: #000;
--bs-carousel-control-icon-filter: invert(1) grayscale(100);
}
.spinner-grow,
@@ -6773,7 +6752,10 @@ textarea.form-control-lg {
}
.offcanvas-header .btn-close {
padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5);
margin: calc(-0.5 * var(--bs-offcanvas-padding-y)) calc(-0.5 * var(--bs-offcanvas-padding-x)) calc(-0.5 * var(--bs-offcanvas-padding-y)) auto;
margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y));
margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x));
margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y));
margin-left: auto;
}
.offcanvas-title {
@@ -7174,6 +7156,10 @@ textarea.form-control-lg {
.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {
position: absolute !important;
}
.visually-hidden *,
.visually-hidden-focusable:not(:focus):not(:focus-within) * {
overflow: hidden !important;
}
.stretched-link::after {
position: absolute;

View File

@@ -4,13 +4,12 @@
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#bs5/dt-2.1.8
* https://datatables.net/download/#bs5/dt-2.3.1
*
* Included libraries:
* DataTables 2.1.8
* DataTables 2.3.1
*/
@charset "UTF-8";
:root {
--dt-row-selected: 13, 110, 253;
--dt-row-selected-text: 255, 255, 255;
@@ -43,6 +42,9 @@ table.dataTable tr.dt-hasChild td.dt-control:before {
border-bottom: 0px solid transparent;
border-right: 5px solid transparent;
}
table.dataTable tfoot:empty {
display: none;
}
html.dark table.dataTable td.dt-control:before,
:root[data-bs-theme=dark] table.dataTable td.dt-control:before,
@@ -90,8 +92,8 @@ table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before {
position: absolute;
display: block;
bottom: 50%;
content: "";
content: ""/"";
content: "\25B2";
content: "\25B2"/"";
}
table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after,
@@ -99,27 +101,17 @@ table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after {
position: absolute;
display: block;
top: 50%;
content: "";
content: ""/"";
}
table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, table.dataTable thead > tr > th.dt-ordering-asc, table.dataTable thead > tr > th.dt-ordering-desc,
table.dataTable thead > tr > td.dt-orderable-asc,
table.dataTable thead > tr > td.dt-orderable-desc,
table.dataTable thead > tr > td.dt-ordering-asc,
table.dataTable thead > tr > td.dt-ordering-desc {
position: relative;
padding-right: 30px;
content: "\25BC";
content: "\25BC"/"";
}
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order,
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order,
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order,
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order,
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order {
position: absolute;
right: 12px;
top: 0;
bottom: 0;
position: relative;
width: 12px;
height: 20px;
}
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before,
@@ -161,6 +153,40 @@ table.dataTable thead > tr > td:active {
outline: none;
}
table.dataTable thead > tr > th div.dt-column-header,
table.dataTable thead > tr > th div.dt-column-footer,
table.dataTable thead > tr > td div.dt-column-header,
table.dataTable thead > tr > td div.dt-column-footer,
table.dataTable tfoot > tr > th div.dt-column-header,
table.dataTable tfoot > tr > th div.dt-column-footer,
table.dataTable tfoot > tr > td div.dt-column-header,
table.dataTable tfoot > tr > td div.dt-column-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 4px;
}
table.dataTable thead > tr > th div.dt-column-header span.dt-column-title,
table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title,
table.dataTable thead > tr > td div.dt-column-header span.dt-column-title,
table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title,
table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title,
table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title,
table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title,
table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title {
flex-grow: 1;
}
table.dataTable thead > tr > th div.dt-column-header span.dt-column-title:empty,
table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title:empty,
table.dataTable thead > tr > td div.dt-column-header span.dt-column-title:empty,
table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title:empty,
table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title:empty,
table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title:empty,
table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title:empty,
table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title:empty {
display: none;
}
div.dt-scroll-body > table.dataTable > thead > tr > th,
div.dt-scroll-body > table.dataTable > thead > tr > td {
overflow: hidden;
@@ -251,10 +277,30 @@ table.dataTable th,
table.dataTable td {
box-sizing: border-box;
}
table.dataTable th.dt-type-numeric, table.dataTable th.dt-type-date,
table.dataTable td.dt-type-numeric,
table.dataTable td.dt-type-date {
text-align: right;
}
table.dataTable th.dt-type-numeric div.dt-column-header,
table.dataTable th.dt-type-numeric div.dt-column-footer, table.dataTable th.dt-type-date div.dt-column-header,
table.dataTable th.dt-type-date div.dt-column-footer,
table.dataTable td.dt-type-numeric div.dt-column-header,
table.dataTable td.dt-type-numeric div.dt-column-footer,
table.dataTable td.dt-type-date div.dt-column-header,
table.dataTable td.dt-type-date div.dt-column-footer {
flex-direction: row-reverse;
}
table.dataTable th.dt-left,
table.dataTable td.dt-left {
text-align: left;
}
table.dataTable th.dt-left div.dt-column-header,
table.dataTable th.dt-left div.dt-column-footer,
table.dataTable td.dt-left div.dt-column-header,
table.dataTable td.dt-left div.dt-column-footer {
flex-direction: row;
}
table.dataTable th.dt-center,
table.dataTable td.dt-center {
text-align: center;
@@ -263,10 +309,22 @@ table.dataTable th.dt-right,
table.dataTable td.dt-right {
text-align: right;
}
table.dataTable th.dt-right div.dt-column-header,
table.dataTable th.dt-right div.dt-column-footer,
table.dataTable td.dt-right div.dt-column-header,
table.dataTable td.dt-right div.dt-column-footer {
flex-direction: row-reverse;
}
table.dataTable th.dt-justify,
table.dataTable td.dt-justify {
text-align: justify;
}
table.dataTable th.dt-justify div.dt-column-header,
table.dataTable th.dt-justify div.dt-column-footer,
table.dataTable td.dt-justify div.dt-column-header,
table.dataTable td.dt-justify div.dt-column-footer {
flex-direction: row;
}
table.dataTable th.dt-nowrap,
table.dataTable td.dt-nowrap {
white-space: nowrap;
@@ -276,11 +334,6 @@ table.dataTable td.dt-empty {
text-align: center;
vertical-align: top;
}
table.dataTable th.dt-type-numeric, table.dataTable th.dt-type-date,
table.dataTable td.dt-type-numeric,
table.dataTable td.dt-type-date {
text-align: right;
}
table.dataTable thead th,
table.dataTable thead td,
table.dataTable tfoot th,
@@ -293,6 +346,16 @@ table.dataTable tfoot th.dt-head-left,
table.dataTable tfoot td.dt-head-left {
text-align: left;
}
table.dataTable thead th.dt-head-left div.dt-column-header,
table.dataTable thead th.dt-head-left div.dt-column-footer,
table.dataTable thead td.dt-head-left div.dt-column-header,
table.dataTable thead td.dt-head-left div.dt-column-footer,
table.dataTable tfoot th.dt-head-left div.dt-column-header,
table.dataTable tfoot th.dt-head-left div.dt-column-footer,
table.dataTable tfoot td.dt-head-left div.dt-column-header,
table.dataTable tfoot td.dt-head-left div.dt-column-footer {
flex-direction: row;
}
table.dataTable thead th.dt-head-center,
table.dataTable thead td.dt-head-center,
table.dataTable tfoot th.dt-head-center,
@@ -305,12 +368,32 @@ table.dataTable tfoot th.dt-head-right,
table.dataTable tfoot td.dt-head-right {
text-align: right;
}
table.dataTable thead th.dt-head-right div.dt-column-header,
table.dataTable thead th.dt-head-right div.dt-column-footer,
table.dataTable thead td.dt-head-right div.dt-column-header,
table.dataTable thead td.dt-head-right div.dt-column-footer,
table.dataTable tfoot th.dt-head-right div.dt-column-header,
table.dataTable tfoot th.dt-head-right div.dt-column-footer,
table.dataTable tfoot td.dt-head-right div.dt-column-header,
table.dataTable tfoot td.dt-head-right div.dt-column-footer {
flex-direction: row-reverse;
}
table.dataTable thead th.dt-head-justify,
table.dataTable thead td.dt-head-justify,
table.dataTable tfoot th.dt-head-justify,
table.dataTable tfoot td.dt-head-justify {
text-align: justify;
}
table.dataTable thead th.dt-head-justify div.dt-column-header,
table.dataTable thead th.dt-head-justify div.dt-column-footer,
table.dataTable thead td.dt-head-justify div.dt-column-header,
table.dataTable thead td.dt-head-justify div.dt-column-footer,
table.dataTable tfoot th.dt-head-justify div.dt-column-header,
table.dataTable tfoot th.dt-head-justify div.dt-column-footer,
table.dataTable tfoot td.dt-head-justify div.dt-column-header,
table.dataTable tfoot td.dt-head-justify div.dt-column-footer {
flex-direction: row;
}
table.dataTable thead th.dt-head-nowrap,
table.dataTable thead td.dt-head-nowrap,
table.dataTable tfoot th.dt-head-nowrap,
@@ -408,6 +491,9 @@ div.dt-container div.dt-layout-table > div {
margin-left: 0;
}
}
div.dt-container {
position: relative;
}
div.dt-container div.dt-length label {
font-weight: normal;
text-align: left;
@@ -496,14 +582,19 @@ table.dataTable.table-sm > thead > tr td.dt-orderable-asc,
table.dataTable.table-sm > thead > tr td.dt-orderable-desc,
table.dataTable.table-sm > thead > tr td.dt-ordering-asc,
table.dataTable.table-sm > thead > tr td.dt-ordering-desc {
padding-right: 20px;
padding-right: 0.25rem;
}
table.dataTable.table-sm > thead > tr th.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc span.dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-orderable-asc span.dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-orderable-desc span.dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-ordering-asc span.dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-ordering-desc span.dt-column-order {
right: 5px;
right: 0.25rem;
}
table.dataTable.table-sm > thead > tr th.dt-type-date span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric span.dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-type-date span.dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-type-numeric span.dt-column-order {
left: 0.25rem;
}
div.dt-scroll-head table.table-bordered {

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@
<dt class="col-sm-5">Web Installed
<span class="badge bg-success d-none" id="web-success" title="Latest version is installed.">Ok</span>
<span class="badge bg-warning text-dark d-none" id="web-warning" title="There seems to be an update available.">Update</span>
<span class="badge bg-info text-dark d-none" id="web-prerelease" title="You seem to be using a pre-release version.">Pre-Release</span>
</dt>
<dd class="col-sm-7">
<span id="web-installed">{{page_data.web_vault_version}}</span>
@@ -68,10 +69,14 @@
<span class="d-block"><b>No</b></span>
{{/unless}}
</dd>
<dt class="col-sm-5">Environment settings overridden</dt>
<dt class="col-sm-5">Uses config.json
{{#if page_data.overrides}}
<span class="badge bg-info text-dark" title="Environment variables are overwritten by a config.json.">Note</span>
{{/if}}
</dt>
<dd class="col-sm-7">
{{#if page_data.overrides}}
<span class="d-block" title="The following settings are overridden: {{page_data.overrides}}"><b>Yes</b></span>
<abbr class="d-block" title="The following settings are overridden: {{page_data.overrides}}"><b>Yes</b></abbr>
{{/if}}
{{#unless page_data.overrides}}
<span class="d-block"><b>No</b></span>
@@ -154,7 +159,11 @@
<dd class="col-sm-7">
<span id="dns-resolved">{{page_data.dns_resolved}}</span>
</dd>
<dt class="col-sm-5">Date & Time (Local)</dt>
<dt class="col-sm-5">Date & Time (Local)
{{#if page_data.tz_env}}
<span class="badge bg-success" title="Configured TZ environment variable">{{page_data.tz_env}}</span>
{{/if}}
</dt>
<dd class="col-sm-7">
<span><b>Server:</b> {{page_data.server_time_local}}</span>
</dd>

View File

@@ -43,7 +43,7 @@
<span class="d-block"><strong>Groups:</strong> {{group_count}}</span>
<span class="d-block"><strong>Events:</strong> {{event_count}}</span>
</td>
<td class="text-end px-0 small">
<td class="text-end px-1 small">
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{id}}" data-vw-org-name="{{name}}" data-vw-billing-email="{{billingEmail}}">Delete Organization</button><br>
</td>
</tr>

View File

@@ -60,7 +60,7 @@
{{/each}}
</div>
</td>
<td class="text-end px-0 small">
<td class="text-end px-1 small">
<span data-vw-user-uuid="{{id}}" data-vw-user-email="{{email}}">
{{#if twoFactorEnabled}}
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br>

View File

@@ -0,0 +1,6 @@
Your Email Change
<!---------------->
A user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). An account already exists with this email ({{ existing_address }}).
If you did not try to change an email address, contact your administrator.
{{> email/email_footer_text }}

View File

@@ -0,0 +1,16 @@
Your Email Change
<!---------------->
{{> email/email_header }}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
A user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). An account already exists with this email ({{ existing_address }}).
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
If you did not try to change an email address, contact your administrator.
</td>
</tr>
</table>
{{> email/email_footer }}

View File

@@ -0,0 +1,8 @@
Verify Your Email
<!---------------->
Verify this email address to finish creating your account by clicking the link below.
Verify Email Address Now: {{{url}}}
If you did not request to verify your account, you can safely ignore this email.
{{> email/email_footer_text }}

View File

@@ -0,0 +1,24 @@
Verify Your Email
<!---------------->
{{> email/email_header }}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
Verify this email address to finish creating your account by clicking the link below.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
<a href="{{{url}}}"
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
Verify Email Address Now
</a>
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
If you did not request to verify your account, you can safely ignore this email.
</td>
</tr>
</table>
{{> email/email_footer }}

View File

@@ -1,7 +1,7 @@
Removed from {{{org_name}}}
Your access to {{{org_name}}} has been revoked.
<!---------------->
You have been removed from organization *{{org_name}}* because your account does not have Two-step Login enabled.
Your user account has been removed from the *{{org_name}}* organization because you do not have two-step login configured.
Before you can re-join this organization you need to set up two-step login on your user account.
You can enable Two-step Login in your account settings.
You can enable two-step login in your account settings.
{{> email/email_footer_text }}

View File

@@ -1,15 +1,16 @@
Removed from {{{org_name}}}
Your access to {{{org_name}}} has been revoked.
<!---------------->
{{> email/email_header }}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
You have been removed from organization <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b> because your account does not have Two-step Login enabled.
Your user account has been removed from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b> organization because you do not have two-step login configured.<br>
Before you can re-join this organization you need to set up two-step login on your user account.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
You can enable Two-step Login in your account settings.
You can enable two-step login in your account settings.
</td>
</tr>
</table>

View File

@@ -21,7 +21,15 @@ a[href$="/settings/sponsored-families"] {
}
/* Hide the `Enterprise Single Sign-On` button on the login page */
a[routerlink="/sso"] {
app-root form.ng-untouched button.\!tw-text-primary-600:nth-child(4) {
@extend %vw-hide;
}
/* Hide Log in with passkey on the login page */
app-root form.ng-untouched a[routerlink="/login-with-passkey"] {
@extend %vw-hide;
}
/* Hide the or text followed by the two buttons hidden above */
app-root form.ng-untouched > div:nth-child(1) > div:nth-child(3) > div:nth-child(2) {
@extend %vw-hide;
}
@@ -31,29 +39,13 @@ a[href$="/settings/two-factor"] {
@extend %vw-hide;
}
/* Hide Business Owned checkbox */
app-org-info > form:nth-child(1) > div:nth-child(3) {
@extend %vw-hide;
}
/* Hide the `This account is owned by a business` checkbox and label */
#ownedBusiness,
label[for^="ownedBusiness"] {
@extend %vw-hide;
}
/* Hide Business Name */
app-org-account form div bit-form-field.tw-block:nth-child(3) {
@extend %vw-hide;
}
/* Hide organization plans */
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) {
app-org-account form.ng-untouched:nth-child(5) {
@extend %vw-hide;
}
@@ -73,11 +65,6 @@ bit-dialog div.tw-col-span-4:has(input[formcontrolname*="access"], input[formcon
@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: "";
@@ -93,23 +80,34 @@ bit-nav-logo bit-nav-item .bwi-shield {
/**** END Static Vaultwarden Changes ****/
/**** START Dynamic Vaultwarden Changes ****/
{{#if signup_disabled}}
/* From web vault 2025.1.2 and onwards, the signup button is hidden
when signups are disabled as the web vault checks the /api/config endpoint.
Note that the clients tend to cache this endpoint for about 1 hour, so it might
take a while for the change to take effect. To avoid the button appearing
when it shouldn't, we'll keep this style in place for a couple of versions */
/* Hide the register link on the login screen */
{{#if (webver "<2025.3.0")}}
app-login form div + div + div + div + hr,
app-login form div + div + div + div + hr + p {
@extend %vw-hide;
}
{{else}}
app-root a[routerlink="/signup"] {
@extend %vw-hide;
}
{{/if}}
{{/if}}
{{#unless mail_enabled}}
/* 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) {
.providers-2fa-1 {
@extend %vw-hide;
}
{{/unless}}
{{#unless yubico_enabled}}
/* 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) {
.providers-2fa-3 {
@extend %vw-hide;
}
{{/unless}}

View File

@@ -56,13 +56,17 @@ impl Fairing for AppHeaders {
res.set_raw_header("X-Content-Type-Options", "nosniff");
res.set_raw_header("X-Robots-Tag", "noindex, nofollow");
if !res.headers().get_one("Content-Type").is_some_and(|v| v.starts_with("image/")) {
res.set_raw_header("Cross-Origin-Resource-Policy", "same-origin");
}
// Obsolete in modern browsers, unsafe (XS-Leak), and largely replaced by CSP
res.set_raw_header("X-XSS-Protection", "0");
// The `Cross-Origin-Resource-Policy` header should not be set on images or on the `icon_external` route.
// Otherwise some clients, like the Bitwarden Desktop, will fail to download the icons
if !(res.headers().get_one("Content-Type").is_some_and(|v| v.starts_with("image/"))
|| req.route().is_some_and(|v| v.name.as_deref() == Some("icon_external")))
{
res.set_raw_header("Cross-Origin-Resource-Policy", "same-origin");
}
// Do not send the Content-Security-Policy (CSP) Header and X-Frame-Options for the *-connector.html files.
// This can cause issues when some MFA requests needs to open a popup or page within the clients like WebAuthn, or Duo.
// This is the same behavior as upstream Bitwarden.
@@ -264,8 +268,8 @@ impl Fairing for BetterLogging {
} else {
"http"
};
let addr = format!("{}://{}:{}", &scheme, &config.address, &config.port);
info!(target: "start", "Rocket has launched from {}", addr);
let addr = format!("{scheme}://{}:{}", &config.address, &config.port);
info!(target: "start", "Rocket has launched from {addr}");
}
async fn on_request(&self, request: &mut Request<'_>, _data: &mut Data<'_>) {
@@ -279,8 +283,8 @@ impl Fairing for BetterLogging {
let uri_subpath = uri_path_str.strip_prefix(&CONFIG.domain_path()).unwrap_or(&uri_path_str);
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
match uri.query() {
Some(q) => info!(target: "request", "{} {}?{}", method, uri_path_str, &q[..q.len().min(30)]),
None => info!(target: "request", "{} {}", method, uri_path_str),
Some(q) => info!(target: "request", "{method} {uri_path_str}?{}", &q[..q.len().min(30)]),
None => info!(target: "request", "{method} {uri_path_str}"),
};
}
}
@@ -295,9 +299,9 @@ impl Fairing for BetterLogging {
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
let status = response.status();
if let Some(ref route) = request.route() {
info!(target: "response", "{} => {}", route, status)
info!(target: "response", "{route} => {status}")
} else {
info!(target: "response", "{}", status)
info!(target: "response", "{status}")
}
}
}
@@ -322,7 +326,7 @@ pub fn get_display_size(size: i64) -> String {
}
}
format!("{:.2} {}", size, UNITS[unit_counter])
format!("{size:.2} {}", UNITS[unit_counter])
}
pub fn get_uuid() -> String {
@@ -695,7 +699,7 @@ where
return Err(e);
}
warn!("Can't connect to database, retrying: {:?}", e);
warn!("Can't connect to database, retrying: {e:?}");
sleep(Duration::from_millis(1_000)).await;
}
@@ -748,9 +752,20 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value {
/// Parses the experimental client feature flags string into a HashMap.
pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap<String, bool> {
let feature_states = experimental_client_feature_flags.split(',').map(|f| (f.trim().to_owned(), true)).collect();
feature_states
// These flags could still be configured, but are deprecated and not used anymore
// To prevent old installations from starting filter these out and not error out
const DEPRECATED_FLAGS: &[&str] =
&["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"];
experimental_client_feature_flags
.split(',')
.filter_map(|f| {
let flag = f.trim();
if !flag.is_empty() && !DEPRECATED_FLAGS.contains(&flag) {
return Some((flag.to_owned(), true));
}
None
})
.collect()
}
/// TODO: This is extracted from IpAddr::is_global, which is unstable: