Compare commits

..

1 Commits

Author SHA1 Message Date
Mathijs van Veluw 0a77776a8b Revert "Disable deployments for release env (#7033)"
This reverts commit 8f0e99b875.
2026-04-12 16:57:48 +02:00
30 changed files with 343 additions and 611 deletions
+1
View File
@@ -1,2 +1,3 @@
# Ignore vendored scripts in GitHub stats # Ignore vendored scripts in GitHub stats
src/static/scripts/* linguist-vendored src/static/scripts/* linguist-vendored
+2 -6
View File
@@ -38,9 +38,7 @@ jobs:
docker-build: docker-build:
name: Build Vaultwarden containers name: Build Vaultwarden containers
if: ${{ github.repository == 'dani-garcia/vaultwarden' }} if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
environment: environment: release
name: release
deployment: false
permissions: permissions:
packages: write # Needed to upload packages and artifacts packages: write # Needed to upload packages and artifacts
contents: read contents: read
@@ -251,9 +249,7 @@ jobs:
name: Merge manifests name: Merge manifests
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: docker-build needs: docker-build
environment: environment: release
name: release
deployment: false
permissions: permissions:
packages: write # Needed to upload packages and artifacts packages: write # Needed to upload packages and artifacts
attestations: write # Needed to generate an artifact attestation for a build attestations: write # Needed to generate an artifact attestation for a build
+2 -2
View File
@@ -38,7 +38,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Run Trivy vulnerability scanner - name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
env: env:
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2 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 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
@@ -50,6 +50,6 @@ jobs:
severity: CRITICAL,HIGH severity: CRITICAL,HIGH
- name: Upload Trivy scan results to GitHub Security tab - name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with: with:
sarif_file: 'trivy-results.sarif' sarif_file: 'trivy-results.sarif'
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
- name: Spell Check Repo - name: Spell Check Repo
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1 uses: crate-ci/typos@02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
with: with:
# intentionally not scanning the entire repository, # intentionally not scanning the entire repository,
# since it contains integration tests. # since it contains integration tests.
+53 -55
View File
@@ -1,60 +1,58 @@
--- ---
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
hooks: hooks:
- id: check-yaml - id: check-yaml
- id: check-json - id: check-json
- id: check-toml - id: check-toml
- id: mixed-line-ending - id: mixed-line-ending
args: [ "--fix=no" ] args: ["--fix=no"]
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: "(.*js$|.*css$)" exclude: "(.*js$|.*css$)"
- id: check-case-conflict - id: check-case-conflict
- id: check-merge-conflict - id: check-merge-conflict
- id: detect-private-key - id: detect-private-key
- id: check-symlinks - id: check-symlinks
- id: forbid-submodules - id: forbid-submodules
- repo: local
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
- repo: https://github.com/crate-ci/typos
rev: cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1
hooks: hooks:
- id: typos - id: fmt
name: fmt
- repo: local description: Format files with cargo fmt.
hooks: entry: cargo fmt
- id: fmt language: system
name: fmt always_run: true
description: Format files with cargo fmt. pass_filenames: false
entry: cargo fmt args: ["--", "--check"]
language: system - id: cargo-test
always_run: true name: cargo test
pass_filenames: false description: Test the package for errors.
args: [ "--", "--check" ] entry: cargo test
- id: cargo-test language: system
name: cargo test args: ["--features", "sqlite,mysql,postgresql", "--"]
description: Test the package for errors. types_or: [rust, file]
entry: cargo test files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
language: system pass_filenames: false
args: [ "--features", "sqlite,mysql,postgresql", "--" ] - id: cargo-clippy
types_or: [ rust, file ] name: cargo clippy
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) description: Lint Rust sources
pass_filenames: false entry: cargo clippy
- id: cargo-clippy language: system
name: cargo clippy args: ["--features", "sqlite,mysql,postgresql", "--", "-D", "warnings"]
description: Lint Rust sources types_or: [rust, file]
entry: cargo clippy files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
language: system pass_filenames: false
args: [ "--features", "sqlite,mysql,postgresql", "--", "-D", "warnings" ] - id: check-docker-templates
types_or: [ rust, file ] name: check docker templates
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) description: Check if the Docker templates are updated
pass_filenames: false language: system
- id: check-docker-templates entry: sh
name: check docker templates args:
description: Check if the Docker templates are updated - "-c"
language: system - "cd docker && make"
entry: sh # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
args: - repo: https://github.com/crate-ci/typos
- "-c" rev: 02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0
- "cd docker && make" hooks:
- id: typos
Generated
+152 -355
View File
File diff suppressed because it is too large Load Diff
+8 -10
View File
@@ -1,6 +1,6 @@
[workspace.package] [workspace.package]
edition = "2021" edition = "2021"
rust-version = "1.93.0" rust-version = "1.92.0"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
repository = "https://github.com/dani-garcia/vaultwarden" repository = "https://github.com/dani-garcia/vaultwarden"
publish = false publish = false
@@ -79,7 +79,7 @@ dashmap = "6.1.0"
# Async futures # Async futures
futures = "0.3.32" futures = "0.3.32"
tokio = { version = "1.52.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio = { version = "1.51.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
tokio-util = { version = "0.7.18", features = ["compat"]} tokio-util = { version = "0.7.18", features = ["compat"]}
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
@@ -103,7 +103,7 @@ ring = "0.17.14"
subtle = "2.6.1" subtle = "2.6.1"
# UUID generation # UUID generation
uuid = { version = "1.23.1", features = ["v4"] } uuid = { version = "1.23.0", features = ["v4"] }
# Date and time libraries # Date and time libraries
chrono = { version = "0.4.44", features = ["clock", "serde"], default-features = false } chrono = { version = "0.4.44", features = ["clock", "serde"], default-features = false }
@@ -145,7 +145,7 @@ handlebars = { version = "6.4.0", features = ["dir_source"] }
# HTTP client (Used for favicons, version check, DUO and HIBP API) # HTTP client (Used for favicons, version check, DUO and HIBP API)
reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false} reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
hickory-resolver = "0.26.0" hickory-resolver = "0.25.2"
# Favicon extraction libraries # Favicon extraction libraries
html5gum = "0.8.3" html5gum = "0.8.3"
@@ -162,7 +162,7 @@ cookie = "0.18.1"
cookie_store = "0.22.1" cookie_store = "0.22.1"
# Used by U2F, JWT and PostgreSQL # Used by U2F, JWT and PostgreSQL
openssl = "0.10.78" openssl = "0.10.76"
# CLI argument parsing # CLI argument parsing
pico-args = "0.5.0" pico-args = "0.5.0"
@@ -180,7 +180,7 @@ semver = "1.0.28"
# Allow overriding the default memory allocator # Allow overriding the default memory allocator
# Mainly used for the musl builds, since the default musl malloc is very slow # Mainly used for the musl builds, since the default musl malloc is very slow
mimalloc = { version = "0.1.50", features = ["secure"], default-features = false, optional = true } mimalloc = { version = "0.1.48", features = ["secure"], default-features = false, optional = true }
which = "8.0.2" which = "8.0.2"
@@ -198,9 +198,9 @@ opendal = { version = "0.55.0", features = ["services-fs"], default-features = f
# For retrieving AWS credentials, including temporary SSO credentials # For retrieving AWS credentials, including temporary SSO credentials
anyhow = { version = "1.0.102", optional = true } anyhow = { version = "1.0.102", optional = true }
aws-config = { version = "1.8.16", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } aws-config = { version = "1.8.15", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
aws-credential-types = { version = "1.2.14", optional = true } aws-credential-types = { version = "1.2.14", optional = true }
aws-smithy-runtime-api = { version = "1.12.0", optional = true } aws-smithy-runtime-api = { version = "1.11.6", optional = true }
http = { version = "1.4.0", optional = true } http = { version = "1.4.0", optional = true }
reqsign = { version = "0.16.5", optional = true } reqsign = { version = "0.16.5", optional = true }
@@ -301,7 +301,6 @@ branches_sharing_code = "deny"
case_sensitive_file_extension_comparisons = "deny" case_sensitive_file_extension_comparisons = "deny"
cast_lossless = "deny" cast_lossless = "deny"
clone_on_ref_ptr = "deny" clone_on_ref_ptr = "deny"
duration_suboptimal_units = "deny"
equatable_if_let = "deny" equatable_if_let = "deny"
excessive_precision = "deny" excessive_precision = "deny"
filter_map_next = "deny" filter_map_next = "deny"
@@ -323,7 +322,6 @@ needless_continue = "deny"
needless_lifetimes = "deny" needless_lifetimes = "deny"
option_option = "deny" option_option = "deny"
redundant_clone = "deny" redundant_clone = "deny"
ref_option = "deny"
string_add_assign = "deny" string_add_assign = "deny"
unnecessary_join = "deny" unnecessary_join = "deny"
unnecessary_self_imports = "deny" unnecessary_self_imports = "deny"
+3 -3
View File
@@ -1,11 +1,11 @@
--- ---
vault_version: "v2026.3.1" vault_version: "v2026.2.0"
vault_image_digest: "sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767" vault_image_digest: "sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447"
# Cross Compile Docker Helper Scripts v1.9.0 # Cross Compile Docker Helper Scripts v1.9.0
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # 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 # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707"
rust_version: 1.95.0 # Rust version to be used rust_version: 1.94.1 # Rust version to be used
debian_version: trixie # Debian release name to be used debian_version: trixie # Debian release name to be used
alpine_version: "3.23" # Alpine version to be used alpine_version: "3.23" # Alpine version to be used
# For which platforms/architectures will we try to build images # For which platforms/architectures will we try to build images
+10 -10
View File
@@ -19,23 +19,23 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - 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. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2026.3.1 # $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.3.1 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.2.0
# [docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767] # [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447
# [docker.io/vaultwarden/web-vault:v2026.3.1] # [docker.io/vaultwarden/web-vault:v2026.2.0]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault
########################## ALPINE BUILD IMAGES ########################## ########################## ALPINE BUILD IMAGES ##########################
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64
## And for Alpine we define all build images here, they will only be loaded when actually used ## And for Alpine we define all build images here, they will only be loaded when actually used
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.95.0 AS build_amd64 FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.94.1 AS build_amd64
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.95.0 AS build_arm64 FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.94.1 AS build_arm64
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.95.0 AS build_armv7 FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.94.1 AS build_armv7
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.95.0 AS build_armv6 FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.94.1 AS build_armv6
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006 # hadolint ignore=DL3006
+7 -7
View File
@@ -19,15 +19,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - 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. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2026.3.1 # $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.3.1 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.2.0
# [docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767] # [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447
# [docker.io/vaultwarden/web-vault:v2026.3.1] # [docker.io/vaultwarden/web-vault:v2026.2.0]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault
########################## Cross Compile Docker Helper Scripts ########################## ########################## Cross Compile Docker Helper Scripts ##########################
## We use the linux/amd64 no matter which Build Platform, since these are all bash 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:c64defb9ed5a91eacb37f
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006 # hadolint ignore=DL3006
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.95.0-slim-trixie AS build FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.94.1-slim-trixie AS build
COPY --from=xx / / COPY --from=xx / /
ARG TARGETARCH ARG TARGETARCH
ARG TARGETVARIANT ARG TARGETVARIANT
+1 -1
View File
@@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "1.95.0" channel = "1.94.1"
components = [ "rustfmt", "clippy" ] components = [ "rustfmt", "clippy" ]
profile = "minimal" profile = "minimal"
+3 -12
View File
@@ -30,7 +30,6 @@ use crate::{
error::{Error, MapResult}, error::{Error, MapResult},
http_client::make_http_request, http_client::make_http_request,
mail, mail,
sso::FAKE_SSO_IDENTIFIER,
util::{ util::{
container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size, container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size,
is_running_in_container, parse_experimental_client_feature_flags, FeatureFlagFilter, NumberOrString, is_running_in_container, parse_experimental_client_feature_flags, FeatureFlagFilter, NumberOrString,
@@ -316,11 +315,7 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -
async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult { async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult {
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {
let org_id: OrganizationId = if CONFIG.sso_enabled() { let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
FAKE_SSO_IDENTIFIER.into()
} else {
FAKE_ADMIN_UUID.into()
};
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into(); let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
} else { } else {
@@ -469,7 +464,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti
if CONFIG.push_enabled() { if CONFIG.push_enabled() {
for device in Device::find_push_devices_by_user(&user.uuid, &conn).await { for device in Device::find_push_devices_by_user(&user.uuid, &conn).await {
match unregister_push_device(device.push_uuid.as_ref()).await { match unregister_push_device(&device.push_uuid).await {
Ok(r) => r, 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}"),
}; };
@@ -523,11 +518,7 @@ async fn resend_user_invite(user_id: UserId, _token: AdminToken, conn: DbConn) -
} }
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {
let org_id: OrganizationId = if CONFIG.sso_enabled() { let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
FAKE_SSO_IDENTIFIER.into()
} else {
FAKE_ADMIN_UUID.into()
};
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into(); let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
} else { } else {
+10 -10
View File
@@ -137,7 +137,7 @@ struct KeysData {
} }
/// Trims whitespace from password hints, and converts blank password hints to `None`. /// Trims whitespace from password hints, and converts blank password hints to `None`.
fn clean_password_hint(password_hint: Option<&String>) -> Option<String> { fn clean_password_hint(password_hint: &Option<String>) -> Option<String> {
match password_hint { match password_hint {
None => None, None => None,
Some(h) => match h.trim() { Some(h) => match h.trim() {
@@ -147,7 +147,7 @@ fn clean_password_hint(password_hint: Option<&String>) -> Option<String> {
} }
} }
fn enforce_password_hint_setting(password_hint: Option<&String>) -> EmptyResult { fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult {
if password_hint.is_some() && !CONFIG.password_hints_allowed() { if password_hint.is_some() && !CONFIG.password_hints_allowed() {
err!("Password hints have been disabled by the administrator. Remove the hint and try again."); err!("Password hints have been disabled by the administrator. Remove the hint and try again.");
} }
@@ -245,8 +245,8 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, conn:
// Check against the password hint setting here so if it fails, the user // Check against the password hint setting here so if it fails, the user
// can retry without losing their invitation below. // can retry without losing their invitation below.
let password_hint = clean_password_hint(data.master_password_hint.as_ref()); let password_hint = clean_password_hint(&data.master_password_hint);
enforce_password_hint_setting(password_hint.as_ref())?; enforce_password_hint_setting(&password_hint)?;
let mut user = match User::find_by_mail(&email, &conn).await { let mut user = match User::find_by_mail(&email, &conn).await {
Some(user) => { Some(user) => {
@@ -353,8 +353,8 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
// Check against the password hint setting here so if it fails, // Check against the password hint setting here so if it fails,
// the user can retry without losing their invitation below. // the user can retry without losing their invitation below.
let password_hint = clean_password_hint(data.master_password_hint.as_ref()); let password_hint = clean_password_hint(&data.master_password_hint);
enforce_password_hint_setting(password_hint.as_ref())?; enforce_password_hint_setting(&password_hint)?;
set_kdf_data(&mut user, &data.kdf)?; set_kdf_data(&mut user, &data.kdf)?;
@@ -374,7 +374,7 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
} }
if let Some(identifier) = data.org_identifier { if let Some(identifier) = data.org_identifier {
if identifier != crate::sso::FAKE_SSO_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID { if identifier != crate::sso::FAKE_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID {
let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else { let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else {
err!("Failed to retrieve the associated organization") err!("Failed to retrieve the associated organization")
}; };
@@ -515,8 +515,8 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbCon
err!("Invalid password") err!("Invalid password")
} }
user.password_hint = clean_password_hint(data.master_password_hint.as_ref()); user.password_hint = clean_password_hint(&data.master_password_hint);
enforce_password_hint_setting(user.password_hint.as_ref())?; enforce_password_hint_setting(&user.password_hint)?;
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn) log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)
.await; .await;
@@ -1438,7 +1438,7 @@ async fn put_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResul
if let Some(device) = Device::find_by_uuid(&device_id, &conn).await { if let Some(device) = Device::find_by_uuid(&device_id, &conn).await {
Device::clear_push_token_by_uuid(&device_id, &conn).await?; Device::clear_push_token_by_uuid(&device_id, &conn).await?;
unregister_push_device(device.push_uuid.as_ref()).await?; unregister_push_device(&device.push_uuid).await?;
} }
Ok(()) Ok(())
+7 -7
View File
@@ -630,7 +630,7 @@ async fn post_ciphers_import(data: Json<ImportData>, headers: Headers, conn: DbC
let mut user = headers.user; let mut user = headers.user;
user.update_revision(&conn).await?; user.update_revision(&conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await; nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
Ok(()) Ok(())
} }
@@ -1005,7 +1005,7 @@ async fn put_cipher_share_selected(
} }
// Multi share actions do not send out a push for each cipher, we need to send a general sync here // Multi share actions do not send out a push for each cipher, we need to send a general sync here
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await; nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;
Ok(()) Ok(())
} }
@@ -1618,7 +1618,7 @@ async fn move_cipher_selected(
.await; .await;
} else { } else {
// Multi move actions do not send out a push for each cipher, we need to send a general sync here // Multi move actions do not send out a push for each cipher, we need to send a general sync here
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await; nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;
} }
if cipher_count != accessible_ciphers_count { if cipher_count != accessible_ciphers_count {
@@ -1670,7 +1670,7 @@ async fn purge_org_vault(
match Membership::find_confirmed_by_user_and_org(&user.uuid, &organization.org_id, &conn).await { match Membership::find_confirmed_by_user_and_org(&user.uuid, &organization.org_id, &conn).await {
Some(member) if member.atype == MembershipType::Owner => { Some(member) if member.atype == MembershipType::Owner => {
Cipher::delete_all_by_organization(&organization.org_id, &conn).await?; Cipher::delete_all_by_organization(&organization.org_id, &conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await; nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
log_event( log_event(
EventType::OrganizationPurgedVault as i32, EventType::OrganizationPurgedVault as i32,
@@ -1710,7 +1710,7 @@ async fn purge_personal_vault(
} }
user.update_revision(&conn).await?; user.update_revision(&conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await; nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
Ok(()) Ok(())
} }
@@ -1805,7 +1805,7 @@ async fn _delete_multiple_ciphers(
} }
// Multi delete actions do not send out a push for each cipher, we need to send a general sync here // Multi delete actions do not send out a push for each cipher, we need to send a general sync here
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await; nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;
Ok(()) Ok(())
} }
@@ -1873,7 +1873,7 @@ async fn _restore_multiple_ciphers(
} }
// Multi move actions do not send out a push for each cipher, we need to send a general sync here // Multi move actions do not send out a push for each cipher, we need to send a general sync here
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), conn).await; nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await;
Ok(Json(json!({ Ok(Json(json!({
"data": ciphers, "data": ciphers,
+1 -1
View File
@@ -124,7 +124,7 @@ async fn post_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: Db
user.save(&conn).await?; user.save(&conn).await?;
nt.send_user_update(UpdateType::SyncSettings, &user, headers.device.push_uuid.as_ref(), &conn).await; nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &conn).await;
Ok(Json(json!({}))) Ok(Json(json!({})))
} }
+10 -32
View File
@@ -20,8 +20,7 @@ use crate::{
DbConn, DbConn,
}, },
mail, mail,
sso::FAKE_SSO_IDENTIFIER, util::{convert_json_key_lcase_first, get_uuid, NumberOrString},
util::{convert_json_key_lcase_first, NumberOrString},
CONFIG, CONFIG,
}; };
@@ -65,7 +64,6 @@ pub fn routes() -> Vec<Route> {
post_org_import, post_org_import,
list_policies, list_policies,
list_policies_token, list_policies_token,
get_dummy_master_password_policy,
get_master_password_policy, get_master_password_policy,
get_policy, get_policy,
put_policy, put_policy,
@@ -101,7 +99,6 @@ pub fn routes() -> Vec<Route> {
get_billing_metadata, get_billing_metadata,
get_billing_warnings, get_billing_warnings,
get_auto_enroll_status, get_auto_enroll_status,
get_self_host_billing_metadata,
] ]
} }
@@ -356,7 +353,7 @@ async fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> {
// The returned `Id` will then be passed to `get_master_password_policy` which will mainly ignore it // The returned `Id` will then be passed to `get_master_password_policy` which will mainly ignore it
#[get("/organizations/<identifier>/auto-enroll-status")] #[get("/organizations/<identifier>/auto-enroll-status")]
async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn) -> JsonResult { async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn) -> JsonResult {
let org = if identifier == FAKE_SSO_IDENTIFIER { let org = if identifier == crate::sso::FAKE_IDENTIFIER {
match Membership::find_main_user_org(&headers.user.uuid, &conn).await { match Membership::find_main_user_org(&headers.user.uuid, &conn).await {
Some(member) => Organization::find_by_uuid(&member.org_uuid, &conn).await, Some(member) => Organization::find_by_uuid(&member.org_uuid, &conn).await,
None => None, None => None,
@@ -366,7 +363,7 @@ async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn
}; };
let (id, identifier, rp_auto_enroll) = match org { let (id, identifier, rp_auto_enroll) = match org {
None => (identifier.to_string(), identifier.to_string(), false), None => (get_uuid(), identifier.to_string(), false),
Some(org) => ( Some(org) => (
org.uuid.to_string(), org.uuid.to_string(),
org.uuid.to_string(), org.uuid.to_string(),
@@ -927,7 +924,7 @@ async fn get_org_domain_sso_verified(data: Json<OrgDomainDetails>, conn: DbConn)
.collect::<Vec<(String, String)>>() .collect::<Vec<(String, String)>>()
{ {
v if !v.is_empty() => v, v if !v.is_empty() => v,
_ => vec![(FAKE_SSO_IDENTIFIER.to_string(), FAKE_SSO_IDENTIFIER.to_string())], _ => vec![(crate::sso::FAKE_IDENTIFIER.to_string(), crate::sso::FAKE_IDENTIFIER.to_string())],
}; };
Ok(Json(json!({ Ok(Json(json!({
@@ -1463,7 +1460,7 @@ async fn _confirm_invite(
let save_result = member_to_confirm.save(conn).await; let save_result = member_to_confirm.save(conn).await;
if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await { if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {
nt.send_user_update(UpdateType::SyncOrgKeys, &user, headers.device.push_uuid.as_ref(), conn).await; nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await;
} }
save_result save_result
@@ -1721,7 +1718,7 @@ async fn _delete_member(
.await; .await;
if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await { if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await {
nt.send_user_update(UpdateType::SyncOrgKeys, &user, headers.device.push_uuid.as_ref(), conn).await; nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await;
} }
member_to_delete.delete(conn).await member_to_delete.delete(conn).await
@@ -1978,19 +1975,9 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn)
}))) })))
} }
// Called during the SSO enrollment return the default policy // Called during the SSO enrollment.
#[get("/organizations/00000000-01DC-01DC-01DC-000000000000/policies/master-password", rank = 1)] // Return the org policy if it exists, otherwise use the default one.
fn get_dummy_master_password_policy() -> JsonResult { #[get("/organizations/<org_id>/policies/master-password", rank = 1)]
let (enabled, data) = match CONFIG.sso_master_password_policy_value() {
Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()),
_ => (false, "null".to_string()),
};
let policy = OrgPolicy::new(FAKE_SSO_IDENTIFIER.into(), OrgPolicyType::MasterPassword, enabled, data);
Ok(Json(policy.to_json()))
}
// Called during the SSO enrollment return the org policy if it exists
#[get("/organizations/<org_id>/policies/master-password", rank = 2)]
async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
let policy = let policy =
OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| { OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| {
@@ -2005,7 +1992,7 @@ async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberH
Ok(Json(policy.to_json())) Ok(Json(policy.to_json()))
} }
#[get("/organizations/<org_id>/policies/<pol_type>", rank = 3)] #[get("/organizations/<org_id>/policies/<pol_type>", rank = 2)]
async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, conn: DbConn) -> JsonResult { async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, conn: DbConn) -> JsonResult {
if org_id != headers.org_id { if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match"); err!("Organization not found", "Organization id's do not match");
@@ -2214,15 +2201,6 @@ fn get_billing_warnings(_org_id: OrganizationId, _headers: OrgMemberHeaders) ->
})) }))
} }
#[get("/organizations/<_org_id>/billing/vnext/self-host/metadata")]
fn get_self_host_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json<Value> {
// Prevent a 404 error, which also causes Javascript errors.
Json(json!({
"isOnSecretsManagerStandalone": false, // Secrets Manager is not supported by Vaultwarden
"organizationOccupiedSeats": 0 // Vaultwarden does not count seats
}))
}
fn _empty_data_json() -> Value { fn _empty_data_json() -> Value {
json!({ json!({
"object": "list", "object": "list",
+1 -1
View File
@@ -574,7 +574,7 @@ async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Re
Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host)) Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host))
} else { } else {
Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_mins(5)).await?.uri().to_string()) Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_secs(5 * 60)).await?.uri().to_string())
} }
} }
+1 -1
View File
@@ -38,7 +38,7 @@ static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin) let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)
.expect("Creating WebauthnBuilder failed") .expect("Creating WebauthnBuilder failed")
.rp_name(&domain) .rp_name(&domain)
.timeout(Duration::from_mins(1)); .timeout(Duration::from_millis(60000));
webauthn.build().expect("Building Webauthn failed") webauthn.build().expect("Building Webauthn failed")
}); });
+36 -49
View File
@@ -2,6 +2,7 @@ use chrono::Utc;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use rocket::{ use rocket::{
form::{Form, FromForm}, form::{Form, FromForm},
http::Status,
response::Redirect, response::Redirect,
serde::json::Json, serde::json::Json,
Route, Route,
@@ -11,7 +12,7 @@ use serde_json::Value;
use crate::{ use crate::{
api::{ api::{
core::{ core::{
accounts::{_prelogin, _register, kdf_upgrade, PreloginData, RegisterData}, accounts::{PreloginData, RegisterData, _prelogin, _register, kdf_upgrade},
log_user_event, log_user_event,
two_factor::{ two_factor::{
authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn, authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn,
@@ -41,7 +42,6 @@ pub fn routes() -> Vec<Route> {
routes![ routes![
login, login,
prelogin, prelogin,
prelogin_password,
identity_register, identity_register,
register_verification_email, register_verification_email,
register_finish, register_finish,
@@ -65,43 +65,43 @@ async fn login(
let login_result = match data.grant_type.as_ref() { let login_result = match data.grant_type.as_ref() {
"refresh_token" => { "refresh_token" => {
_check_is_some(data.refresh_token.as_ref(), "refresh_token cannot be blank")?; _check_is_some(&data.refresh_token, "refresh_token cannot be blank")?;
_refresh_login(data, &conn, &client_header.ip).await _refresh_login(data, &conn, &client_header.ip).await
} }
"password" if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO sign-in is required"), "password" if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO sign-in is required"),
"password" => { "password" => {
_check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; _check_is_some(&data.client_id, "client_id cannot be blank")?;
_check_is_some(data.password.as_ref(), "password cannot be blank")?; _check_is_some(&data.password, "password cannot be blank")?;
_check_is_some(data.scope.as_ref(), "scope cannot be blank")?; _check_is_some(&data.scope, "scope cannot be blank")?;
_check_is_some(data.username.as_ref(), "username cannot be blank")?; _check_is_some(&data.username, "username cannot be blank")?;
_check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; _check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
_check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; _check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?;
_password_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await _password_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await
} }
"client_credentials" => { "client_credentials" => {
_check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; _check_is_some(&data.client_id, "client_id cannot be blank")?;
_check_is_some(data.client_secret.as_ref(), "client_secret cannot be blank")?; _check_is_some(&data.client_secret, "client_secret cannot be blank")?;
_check_is_some(data.scope.as_ref(), "scope cannot be blank")?; _check_is_some(&data.scope, "scope cannot be blank")?;
_check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; _check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
_check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; _check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?;
_api_key_login(data, &mut user_id, &conn, &client_header.ip).await _api_key_login(data, &mut user_id, &conn, &client_header.ip).await
} }
"authorization_code" if CONFIG.sso_enabled() => { "authorization_code" if CONFIG.sso_enabled() => {
_check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; _check_is_some(&data.client_id, "client_id cannot be blank")?;
_check_is_some(data.code.as_ref(), "code cannot be blank")?; _check_is_some(&data.code, "code cannot be blank")?;
_check_is_some(data.code_verifier.as_ref(), "code verifier cannot be blank")?; _check_is_some(&data.code_verifier, "code verifier cannot be blank")?;
_check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; _check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
_check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; _check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?;
_sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await _sso_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await
} }
"authorization_code" => err!("SSO sign-in is not available"), "authorization_code" => err!("SSO sign-in is not available"),
t => err!("Invalid type", t), t => err!("Invalid type", t),
@@ -131,14 +131,12 @@ async fn login(
login_result login_result
} }
// Return Status::Unauthorized to trigger logout
async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult { async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {
// When a refresh token is invalid or missing we need to respond with an HTTP BadRequest (400) // Extract token
// It also needs to return a json which holds at least a key `error` with the value `invalid_grant` let refresh_token = match data.refresh_token {
// See the link below for details Some(token) => token,
// https://github.com/bitwarden/clients/blob/2ee158e720a5e7dbe3641caf80b569e97a1dd91b/libs/common/src/services/api.service.ts#L1786-L1797 None => err_code!("Missing refresh_token", Status::Unauthorized.code),
let Some(refresh_token) = data.refresh_token else {
err_json!(json!({"error": "invalid_grant"}), "Missing refresh_token")
}; };
// --- // ---
@@ -149,10 +147,7 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> Json
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await; // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
match auth::refresh_tokens(ip, &refresh_token, data.client_id, conn).await { match auth::refresh_tokens(ip, &refresh_token, data.client_id, conn).await {
Err(err) => { Err(err) => {
err_json!( err_code!(format!("Unable to refresh login credentials: {}", err.message()), Status::Unauthorized.code)
json!({"error": "invalid_grant"}),
format!("Unable to refresh login credentials: {}", err.message())
)
} }
Ok((mut device, auth_tokens)) => { Ok((mut device, auth_tokens)) => {
// Save to update `device.updated_at` to track usage and toggle new status // Save to update `device.updated_at` to track usage and toggle new status
@@ -177,7 +172,7 @@ async fn _sso_login(
user_id: &mut Option<UserId>, user_id: &mut Option<UserId>,
conn: &DbConn, conn: &DbConn,
ip: &ClientIp, ip: &ClientIp,
client_version: Option<&ClientVersion>, client_version: &Option<ClientVersion>,
) -> JsonResult { ) -> JsonResult {
AuthMethod::Sso.check_scope(data.scope.as_ref())?; AuthMethod::Sso.check_scope(data.scope.as_ref())?;
@@ -320,7 +315,7 @@ async fn _password_login(
user_id: &mut Option<UserId>, user_id: &mut Option<UserId>,
conn: &DbConn, conn: &DbConn,
ip: &ClientIp, ip: &ClientIp,
client_version: Option<&ClientVersion>, client_version: &Option<ClientVersion>,
) -> JsonResult { ) -> JsonResult {
// Validate scope // Validate scope
AuthMethod::Password.check_scope(data.scope.as_ref())?; AuthMethod::Password.check_scope(data.scope.as_ref())?;
@@ -734,7 +729,7 @@ async fn twofactor_auth(
data: &ConnectData, data: &ConnectData,
device: &mut Device, device: &mut Device,
ip: &ClientIp, ip: &ClientIp,
client_version: Option<&ClientVersion>, client_version: &Option<ClientVersion>,
conn: &DbConn, conn: &DbConn,
) -> ApiResult<Option<String>> { ) -> ApiResult<Option<String>> {
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await; let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
@@ -759,10 +754,7 @@ async fn twofactor_auth(
} }
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one
// Ignore Remember and RecoveryCode Types during this check, these are special if !twofactor_ids.contains(&selected_id) {
if ![TwoFactorType::Remember as i32, TwoFactorType::RecoveryCode as i32].contains(&selected_id)
&& !twofactor_ids.contains(&selected_id)
{
err_json!( err_json!(
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"Invalid two factor provider" "Invalid two factor provider"
@@ -879,7 +871,7 @@ async fn _json_err_twofactor(
providers: &[i32], providers: &[i32],
user_id: &UserId, user_id: &UserId,
data: &ConnectData, data: &ConnectData,
client_version: Option<&ClientVersion>, client_version: &Option<ClientVersion>,
conn: &DbConn, conn: &DbConn,
) -> ApiResult<Value> { ) -> ApiResult<Value> {
let mut result = json!({ let mut result = json!({
@@ -983,11 +975,6 @@ async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
_prelogin(data, conn).await _prelogin(data, conn).await
} }
#[post("/accounts/prelogin/password", data = "<data>")]
async fn prelogin_password(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
_prelogin(data, conn).await
}
#[post("/accounts/register", data = "<data>")] #[post("/accounts/register", data = "<data>")]
async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult { async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
_register(data, false, conn).await _register(data, false, conn).await
@@ -1114,7 +1101,7 @@ struct ConnectData {
#[field(name = uncased("code_verifier"))] #[field(name = uncased("code_verifier"))]
code_verifier: Option<OIDCCodeVerifier>, code_verifier: Option<OIDCCodeVerifier>,
} }
fn _check_is_some<T>(value: Option<&T>, msg: &str) -> EmptyResult { fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
if value.is_none() { if value.is_none() {
err!(msg) err!(msg)
} }
+1 -1
View File
@@ -338,7 +338,7 @@ impl WebSocketUsers {
} }
// NOTE: The last modified date needs to be updated before calling these methods // NOTE: The last modified date needs to be updated before calling these methods
pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: Option<&PushId>, conn: &DbConn) { pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &DbConn) {
// Skip any processing if both WebSockets and Push are not active // Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED { if *NOTIFICATIONS_DISABLED {
return; return;
+2 -2
View File
@@ -135,7 +135,7 @@ pub async fn register_push_device(device: &mut Device, conn: &DbConn) -> EmptyRe
Ok(()) Ok(())
} }
pub async fn unregister_push_device(push_id: Option<&PushId>) -> EmptyResult { pub async fn unregister_push_device(push_id: &Option<PushId>) -> EmptyResult {
if !CONFIG.push_enabled() || push_id.is_none() { if !CONFIG.push_enabled() || push_id.is_none() {
return Ok(()); return Ok(());
} }
@@ -206,7 +206,7 @@ pub async fn push_logout(user: &User, acting_device: Option<&Device>, conn: &DbC
} }
} }
pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: Option<&PushId>, conn: &DbConn) { pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &DbConn) {
if Device::check_user_has_push_device(&user.uuid, conn).await { if Device::check_user_has_push_device(&user.uuid, conn).await {
tokio::task::spawn(send_to_push_relay(json!({ tokio::task::spawn(send_to_push_relay(json!({
"userId": user.uuid, "userId": user.uuid,
+3 -3
View File
@@ -1076,7 +1076,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
validate_internal_sso_issuer_url(&cfg.sso_authority)?; validate_internal_sso_issuer_url(&cfg.sso_authority)?;
validate_internal_sso_redirect_url(&cfg.sso_callback_path)?; validate_internal_sso_redirect_url(&cfg.sso_callback_path)?;
validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?; validate_sso_master_password_policy(&cfg.sso_master_password_policy)?;
} }
if cfg._enable_yubico { if cfg._enable_yubico {
@@ -1271,7 +1271,7 @@ fn validate_internal_sso_redirect_url(sso_callback_path: &String) -> Result<open
} }
fn validate_sso_master_password_policy( fn validate_sso_master_password_policy(
sso_master_password_policy: Option<&String>, sso_master_password_policy: &Option<String>,
) -> Result<Option<serde_json::Value>, Error> { ) -> Result<Option<serde_json::Value>, Error> {
let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp)); let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp));
@@ -1725,7 +1725,7 @@ impl Config {
} }
pub fn sso_master_password_policy_value(&self) -> Option<serde_json::Value> { pub fn sso_master_password_policy_value(&self) -> Option<serde_json::Value> {
validate_sso_master_password_policy(self.sso_master_password_policy().as_ref()).ok().flatten() validate_sso_master_password_policy(&self.sso_master_password_policy()).ok().flatten()
} }
pub fn sso_scopes_vec(&self) -> Vec<String> { pub fn sso_scopes_vec(&self) -> Vec<String> {
+1 -1
View File
@@ -50,7 +50,7 @@ impl Attachment {
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone())); let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id)) Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id))
} else { } else {
Ok(operator.presign_read(&self.get_file_path(), Duration::from_mins(5)).await?.uri().to_string()) Ok(operator.presign_read(&self.get_file_path(), Duration::from_secs(5 * 60)).await?.uri().to_string())
} }
} }
+1 -4
View File
@@ -25,7 +25,7 @@ pub struct Device {
pub user_uuid: UserId, pub user_uuid: UserId,
pub name: String, pub name: String,
pub atype: i32, // https://github.com/bitwarden/server/blob/8d547dcc280babab70dd4a3c94ced6a34b12dfbf/src/Core/Enums/DeviceType.cs pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub push_uuid: Option<PushId>, pub push_uuid: Option<PushId>,
pub push_token: Option<String>, pub push_token: Option<String>,
@@ -332,8 +332,6 @@ pub enum DeviceType {
MacOsCLI = 24, MacOsCLI = 24,
#[display("Linux CLI")] #[display("Linux CLI")]
LinuxCLI = 25, LinuxCLI = 25,
#[display("DuckDuckGo")]
DuckDuckGoBrowser = 26,
} }
impl DeviceType { impl DeviceType {
@@ -365,7 +363,6 @@ impl DeviceType {
23 => DeviceType::WindowsCLI, 23 => DeviceType::WindowsCLI,
24 => DeviceType::MacOsCLI, 24 => DeviceType::MacOsCLI,
25 => DeviceType::LinuxCLI, 25 => DeviceType::LinuxCLI,
26 => DeviceType::DuckDuckGoBrowser,
_ => DeviceType::UnknownBrowser, _ => DeviceType::UnknownBrowser,
} }
} }
+21 -24
View File
@@ -6,7 +6,7 @@ use std::{
time::Duration, time::Duration,
}; };
use hickory_resolver::{net::runtime::TokioRuntimeProvider, TokioResolver}; use hickory_resolver::{name_server::TokioConnectionProvider, TokioResolver};
use regex::Regex; use regex::Regex;
use reqwest::{ use reqwest::{
dns::{Name, Resolve, Resolving}, dns::{Name, Resolve, Resolving},
@@ -184,35 +184,35 @@ impl CustomDnsResolver {
} }
fn new() -> Arc<Self> { fn new() -> Arc<Self> {
TokioResolver::builder(TokioRuntimeProvider::default()) match TokioResolver::builder(TokioConnectionProvider::default()) {
.and_then(|mut builder| { Ok(mut builder) => {
// Hickory's default since v0.26 is `Ipv6AndIpv4`, which sorts IPv6 first if CONFIG.dns_prefer_ipv6() {
// This might cause issues on IPv4 only systems or containers builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv6thenIpv4;
// Unless someone enabled DNS_PREFER_IPV6, use Ipv4AndIpv6, which returns IPv4 first which was our previous default
if !CONFIG.dns_prefer_ipv6() {
builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6;
} }
builder.build() let resolver = builder.build();
}) Arc::new(Self::Hickory(Arc::new(resolver)))
.inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}")) }
.map(|resolver| Arc::new(Self::Hickory(Arc::new(resolver)))) Err(e) => {
.unwrap_or_else(|_| Arc::new(Self::Default())) warn!("Error creating Hickory resolver, falling back to default: {e:?}");
Arc::new(Self::Default())
}
}
} }
// Note that we get an iterator of addresses, but we only grab the first one for convenience // Note that we get an iterator of addresses, but we only grab the first one for convenience
async fn resolve_domain(&self, name: &str) -> Result<Vec<SocketAddr>, BoxError> { async fn resolve_domain(&self, name: &str) -> Result<Option<SocketAddr>, BoxError> {
pre_resolve(name)?; pre_resolve(name)?;
let results: Vec<SocketAddr> = match self { let result = match self {
Self::Default() => tokio::net::lookup_host((name, 0)).await?.collect(), Self::Default() => tokio::net::lookup_host(name).await?.next(),
Self::Hickory(r) => r.lookup_ip(name).await?.iter().map(|i| SocketAddr::new(i, 0)).collect(), Self::Hickory(r) => r.lookup_ip(name).await?.iter().next().map(|a| SocketAddr::new(a, 0)),
}; };
for addr in &results { if let Some(addr) = &result {
post_resolve(name, addr.ip())?; post_resolve(name, addr.ip())?;
} }
Ok(results) Ok(result)
} }
} }
@@ -242,11 +242,8 @@ impl Resolve for CustomDnsResolver {
let this = self.clone(); let this = self.clone();
Box::pin(async move { Box::pin(async move {
let name = name.as_str(); let name = name.as_str();
let results = this.resolve_domain(name).await?; let result = this.resolve_domain(name).await?;
if results.is_empty() { Ok::<reqwest::dns::Addrs, _>(Box::new(result.into_iter()))
warn!("Unable to resolve {name} to any valid IP address");
}
Ok::<reqwest::dns::Addrs, _>(Box::new(results.into_iter()))
}) })
} }
} }
+2 -2
View File
@@ -17,7 +17,7 @@ use crate::{
CONFIG, CONFIG,
}; };
pub static FAKE_SSO_IDENTIFIER: &str = "00000000-01DC-01DC-01DC-000000000000"; pub static FAKE_IDENTIFIER: &str = "VW_DUMMY_IDENTIFIER_FOR_OIDC";
static SSO_JWT_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin())); static SSO_JWT_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin()));
@@ -283,7 +283,7 @@ pub async fn exchange_code(
let email_verified = id_claims.email_verified().or(user_info.email_verified()); let email_verified = id_claims.email_verified().or(user_info.email_verified());
let user_name = id_claims.preferred_username().or(user_info.preferred_username()).map(|un| un.to_string()); let user_name = id_claims.preferred_username().map(|un| un.to_string());
let refresh_token = token_response.refresh_token().map(|t| t.secret()); let refresh_token = token_response.refresh_token().map(|t| t.secret());
if refresh_token.is_none() && CONFIG.sso_scopes_vec().contains(&"offline_access".to_string()) { if refresh_token.is_none() && CONFIG.sso_scopes_vec().contains(&"offline_access".to_string()) {
@@ -137,14 +137,6 @@ bit-nav-logo bit-nav-item .bwi-shield {
app-user-layout app-danger-zone button:nth-child(1) { app-user-layout app-danger-zone button:nth-child(1) {
@extend %vw-hide; @extend %vw-hide;
} }
/* Hide unsupported Forwarding email alias options */
ng-dropdown-panel div.ng-dropdown-panel-items div:has(> [title="Firefox Relay"]) {
@extend %vw-hide;
}
ng-dropdown-panel div.ng-dropdown-panel-items div:has(> [title="DuckDuckGo"]) {
@extend %vw-hide;
}
/**** END Static Vaultwarden Changes ****/ /**** END Static Vaultwarden Changes ****/
/**** START Dynamic Vaultwarden Changes ****/ /**** START Dynamic Vaultwarden Changes ****/
{{#if signup_disabled}} {{#if signup_disabled}}
+1 -1
View File
@@ -734,7 +734,7 @@ where
warn!("Can't connect to database, retrying: {e:?}"); warn!("Can't connect to database, retrying: {e:?}");
sleep(Duration::from_secs(1)).await; sleep(Duration::from_millis(1_000)).await;
} }
} }
} }