Compare commits

..

1 Commits

Author SHA1 Message Date
Daniel García
c28eab74dd Cached config operations 2025-12-28 23:44:50 +01:00
19 changed files with 173 additions and 189 deletions

View File

@@ -313,43 +313,45 @@ jobs:
# Determine Base Tags # Determine Base Tags
- name: Determine Base Tags - name: Determine Base Tags
env: env:
BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}"
REF_TYPE: ${{ github.ref_type }} REF_TYPE: ${{ github.ref_type }}
run: | run: |
# Check which main tag we are going to build determined by ref_type # Check which main tag we are going to build determined by ref_type
if [[ "${REF_TYPE}" == "tag" ]]; then if [[ "${REF_TYPE}" == "tag" ]]; then
echo "BASE_TAGS=latest${BASE_IMAGE_TAG},${GITHUB_REF#refs/*/}${BASE_IMAGE_TAG}${BASE_IMAGE_TAG//-/,}" | tee -a "${GITHUB_ENV}" echo "BASE_TAGS=latest,${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_ENV}"
elif [[ "${REF_TYPE}" == "branch" ]]; then elif [[ "${REF_TYPE}" == "branch" ]]; then
echo "BASE_TAGS=testing${BASE_IMAGE_TAG}" | tee -a "${GITHUB_ENV}" echo "BASE_TAGS=testing" | tee -a "${GITHUB_ENV}"
fi fi
- name: Create manifest list, push it and extract digest SHA - name: Create manifest list, push it and extract digest SHA
working-directory: ${{ runner.temp }}/digests working-directory: ${{ runner.temp }}/digests
env: env:
BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}"
BASE_TAGS: "${{ env.BASE_TAGS }}" BASE_TAGS: "${{ env.BASE_TAGS }}"
CONTAINER_REGISTRIES: "${{ env.CONTAINER_REGISTRIES }}" CONTAINER_REGISTRIES: "${{ env.CONTAINER_REGISTRIES }}"
run: | run: |
set +e
IFS=',' read -ra IMAGES <<< "${CONTAINER_REGISTRIES}" IFS=',' read -ra IMAGES <<< "${CONTAINER_REGISTRIES}"
IFS=',' read -ra TAGS <<< "${BASE_TAGS}" IFS=',' read -ra TAGS <<< "${BASE_TAGS}"
TAG_ARGS=()
for img in "${IMAGES[@]}"; do for img in "${IMAGES[@]}"; do
for tag in "${TAGS[@]}"; do for tag in "${TAGS[@]}"; do
TAG_ARGS+=("-t" "${img}:${tag}") echo "Creating manifest for ${img}:${tag}${BASE_IMAGE_TAG}"
OUTPUT=$(docker buildx imagetools create \
-t "${img}:${tag}${BASE_IMAGE_TAG}" \
$(printf "${img}@sha256:%s " *) 2>&1)
STATUS=$?
if [ ${STATUS} -ne 0 ]; then
echo "Manifest creation failed for ${img}:${tag}${BASE_IMAGE_TAG}"
echo "${OUTPUT}"
exit ${STATUS}
fi
echo "Manifest created for ${img}:${tag}${BASE_IMAGE_TAG}"
echo "${OUTPUT}"
done done
done done
set -e
echo "Creating manifest"
if ! OUTPUT=$(docker buildx imagetools create \
"${TAG_ARGS[@]}" \
$(printf "${IMAGES[0]}@sha256:%s " *) 2>&1); then
echo "Manifest creation failed"
echo "${OUTPUT}"
exit 1
fi
echo "Manifest created successfully"
echo "${OUTPUT}"
# Extract digest SHA for subsequent steps # Extract digest SHA for subsequent steps
GET_DIGEST_SHA="$(echo "${OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)" GET_DIGEST_SHA="$(echo "${OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"

View File

@@ -19,4 +19,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@1a319b54cc9e3b333fed6a5c88ba1a90324da514 # v1.40.1 uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0

View File

@@ -53,6 +53,6 @@ repos:
- "cd docker && make" - "cd docker && make"
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: 1a319b54cc9e3b333fed6a5c88ba1a90324da514 # v1.40.1 rev: 2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0
hooks: hooks:
- id: typos - id: typos

26
Cargo.lock generated
View File

@@ -718,9 +718,9 @@ dependencies = [
[[package]] [[package]]
name = "bigdecimal" name = "bigdecimal"
version = "0.4.10" version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"libm", "libm",
@@ -2660,9 +2660,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]] [[package]]
name = "iri-string" name = "iri-string"
version = "0.7.10" version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde", "serde",
@@ -3126,7 +3126,7 @@ dependencies = [
"libc", "libc",
"log", "log",
"openssl", "openssl",
"openssl-probe 0.1.6", "openssl-probe",
"openssl-sys", "openssl-sys",
"schannel", "schannel",
"security-framework 2.11.1", "security-framework 2.11.1",
@@ -3415,12 +3415,6 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-probe"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391"
[[package]] [[package]]
name = "openssl-src" name = "openssl-src"
version = "300.5.4+3.5.4" version = "300.5.4+3.5.4"
@@ -4526,11 +4520,11 @@ dependencies = [
[[package]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.8.3" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923"
dependencies = [ dependencies = [
"openssl-probe 0.2.0", "openssl-probe",
"rustls-pki-types", "rustls-pki-types",
"schannel", "schannel",
"security-framework 3.5.1", "security-framework 3.5.1",
@@ -6630,9 +6624,9 @@ dependencies = [
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.2" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167"
[[package]] [[package]]
name = "zstd" name = "zstd"

View File

@@ -65,14 +65,14 @@ dotenvy = { version = "0.15.7", default-features = false }
# Numerical libraries # Numerical libraries
num-traits = "0.2.19" num-traits = "0.2.19"
num-derive = "0.4.2" num-derive = "0.4.2"
bigdecimal = "0.4.10" bigdecimal = "0.4.9"
# Web framework # Web framework
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false } rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
rocket_ws = { version ="0.1.1" } rocket_ws = { version ="0.1.1" }
# WebSockets libraries # WebSockets libraries
rmpv = "1.3.1" # MessagePack library rmpv = "1.3.0" # MessagePack library
# Concurrent HashMap used for WebSocket messaging and favicons # Concurrent HashMap used for WebSocket messaging and favicons
dashmap = "6.1.0" dashmap = "6.1.0"
@@ -84,7 +84,7 @@ tokio-util = { version = "0.7.17", features = ["compat"]}
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148" serde_json = "1.0.145"
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
# Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility # Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility

View File

@@ -1,6 +1,6 @@
--- ---
vault_version: "v2025.12.1+build.3" vault_version: "v2025.12.0"
vault_image_digest: "sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42" vault_image_digest: "sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613"
# 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

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:v2025.12.1_build.3 # $ docker pull docker.io/vaultwarden/web-vault:v2025.12.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0
# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42] # [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613]
# #
# - 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:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613
# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3] # [docker.io/vaultwarden/web-vault:v2025.12.0]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 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

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:v2025.12.1_build.3 # $ docker pull docker.io/vaultwarden/web-vault:v2025.12.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0
# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42] # [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613]
# #
# - 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:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613
# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3] # [docker.io/vaultwarden/web-vault:v2025.12.0]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 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

View File

@@ -19,13 +19,13 @@
# - 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:{{ vault_version | replace('+', '_') }} # $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version }}
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }} # $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version }}
# [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}] # [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}]
# #
# - 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@{{ vault_image_digest }} # $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
# [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}] # [docker.io/vaultwarden/web-vault:{{ vault_version }}]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault

View File

@@ -31,7 +31,7 @@ use crate::{
http_client::make_http_request, http_client::make_http_request,
mail, mail,
util::{ util::{
container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size, container_base_image, format_naive_datetime_local, get_display_size, get_web_vault_version,
is_running_in_container, NumberOrString, is_running_in_container, NumberOrString,
}, },
CONFIG, VERSION, CONFIG, VERSION,
@@ -689,26 +689,6 @@ async fn get_ntp_time(has_http_access: bool) -> String {
String::from("Unable to fetch NTP time.") String::from("Unable to fetch NTP time.")
} }
fn web_vault_compare(active: &str, latest: &str) -> i8 {
use semver::Version;
use std::cmp::Ordering;
let active_semver = Version::parse(active).unwrap_or_else(|e| {
warn!("Unable to parse active web-vault version '{active}': {e}");
Version::parse("2025.1.1").unwrap()
});
let latest_semver = Version::parse(latest).unwrap_or_else(|e| {
warn!("Unable to parse latest web-vault version '{latest}': {e}");
Version::parse("2025.1.1").unwrap()
});
match active_semver.cmp(&latest_semver) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}
}
#[get("/diagnostics")] #[get("/diagnostics")]
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> { async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
use chrono::prelude::*; use chrono::prelude::*;
@@ -728,21 +708,32 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
_ => "Unable to resolve domain name.".to_string(), _ => "Unable to resolve domain name.".to_string(),
}; };
let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await; let (latest_release, latest_commit, latest_web_build) = get_release_info(has_http_access).await;
let active_web_release = get_active_web_release();
let web_vault_compare = web_vault_compare(&active_web_release, &latest_web_release);
let ip_header_name = &ip_header.0.unwrap_or_default(); let ip_header_name = &ip_header.0.unwrap_or_default();
// 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_vault_pre_release = if let Ok(web_ver_match) = semver::VersionReq::parse(&format!(">{latest_web_build}")) {
web_ver_match.matches(
&semver::Version::parse(&web_vault_version).unwrap_or_else(|_| semver::Version::parse("2025.1.1").unwrap()),
)
} else {
error!("Unable to parse latest_web_build: '{latest_web_build}'");
false
};
let diagnostics_json = json!({ let diagnostics_json = json!({
"dns_resolved": dns_resolved, "dns_resolved": dns_resolved,
"current_release": VERSION, "current_release": VERSION,
"latest_release": latest_vw_release, "latest_release": latest_release,
"latest_commit": latest_vw_commit, "latest_commit": latest_commit,
"web_vault_enabled": &CONFIG.web_vault_enabled(), "web_vault_enabled": &CONFIG.web_vault_enabled(),
"active_web_release": active_web_release, "web_vault_version": web_vault_version,
"latest_web_release": latest_web_release, "latest_web_build": latest_web_build,
"web_vault_compare": web_vault_compare, "web_vault_pre_release": web_vault_pre_release,
"running_within_container": running_within_container, "running_within_container": running_within_container,
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" }, "container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
"has_http_access": has_http_access, "has_http_access": has_http_access,
@@ -853,32 +844,3 @@ impl<'r> FromRequest<'r> for AdminToken {
}) })
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_web_vault_compare() {
// web_vault_compare(active, latest)
// Test normal versions
assert!(web_vault_compare("2025.12.0", "2025.12.1") == -1);
assert!(web_vault_compare("2025.12.1", "2025.12.1") == 0);
assert!(web_vault_compare("2025.12.2", "2025.12.1") == 1);
// Test patched/+build.n versions
// Newer latest version
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1") == -1);
assert!(web_vault_compare("2025.12.1", "2025.12.1+build.1") == -1);
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1") == -1);
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2") == -1);
// Equal versions
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1") == 0);
assert!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2") == 0);
// Newer active version
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1") == 1);
assert!(web_vault_compare("2025.12.2", "2025.12.1+build.1") == 1);
assert!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1") == 1);
assert!(web_vault_compare("2025.12.1+build.3", "2025.12.1+build.2") == 1);
}
}

View File

@@ -919,7 +919,6 @@ struct RegisterVerificationData {
#[derive(rocket::Responder)] #[derive(rocket::Responder)]
enum RegisterVerificationResponse { enum RegisterVerificationResponse {
#[response(status = 204)]
NoContent(()), NoContent(()),
Token(Json<String>), Token(Json<String>),
} }

View File

@@ -47,7 +47,6 @@ pub type EmptyResult = ApiResult<()>;
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct PasswordOrOtpData { struct PasswordOrOtpData {
#[serde(alias = "MasterPasswordHash")]
master_password_hash: Option<String>, master_password_hash: Option<String>,
otp: Option<String>, otp: Option<String>,
} }

View File

@@ -12,6 +12,7 @@ use serde_json::Value;
use crate::{ use crate::{
api::{core::now, ApiResult, EmptyResult}, api::{core::now, ApiResult, EmptyResult},
auth::decode_file_download, auth::decode_file_download,
config::CachedConfigOperation,
db::models::{AttachmentId, CipherId}, db::models::{AttachmentId, CipherId},
error::Error, error::Error,
util::Cached, util::Cached,
@@ -52,19 +53,18 @@ fn not_found() -> ApiResult<Html<String>> {
Ok(Html(text)) Ok(Html(text))
} }
#[get("/css/vaultwarden.css")] static VAULTWARDEN_CSS_CACHE: CachedConfigOperation<String> = CachedConfigOperation::new(|config| {
fn vaultwarden_css() -> Cached<Css<String>> {
let css_options = json!({ let css_options = json!({
"emergency_access_allowed": CONFIG.emergency_access_allowed(), "emergency_access_allowed": config.emergency_access_allowed(),
"load_user_scss": true, "load_user_scss": true,
"mail_2fa_enabled": CONFIG._enable_email_2fa(), "mail_2fa_enabled": config._enable_email_2fa(),
"mail_enabled": CONFIG.mail_enabled(), "mail_enabled": config.mail_enabled(),
"sends_allowed": CONFIG.sends_allowed(), "sends_allowed": config.sends_allowed(),
"signup_disabled": CONFIG.is_signup_disabled(), "signup_disabled": config.is_signup_disabled(),
"sso_enabled": CONFIG.sso_enabled(), "sso_enabled": config.sso_enabled(),
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(), "sso_only": config.sso_enabled() && config.sso_only(),
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(), "yubico_enabled": config._enable_yubico() && config.yubico_client_id().is_some() && config.yubico_secret_key().is_some(),
"webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(), "webauthn_2fa_supported": config.is_webauthn_2fa_supported(),
}); });
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) { let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
@@ -78,7 +78,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
} }
}; };
let css = match grass_compiler::from_string( match grass_compiler::from_string(
scss, scss,
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed), &grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
) { ) {
@@ -97,10 +97,12 @@ fn vaultwarden_css() -> Cached<Css<String>> {
) )
.expect("SCSS to compile") .expect("SCSS to compile")
} }
}; }
});
// Cache for one day should be enough and not too much #[get("/css/vaultwarden.css")]
Cached::ttl(Css(css), 86_400, false) fn vaultwarden_css() -> Css<String> {
Css(CONFIG.cached_operation(&VAULTWARDEN_CSS_CACHE))
} }
#[get("/")] #[get("/")]

View File

@@ -1210,20 +1210,8 @@ pub async fn refresh_tokens(
) -> ApiResult<(Device, AuthTokens)> { ) -> ApiResult<(Device, AuthTokens)> {
let refresh_claims = match decode_refresh(refresh_token) { let refresh_claims = match decode_refresh(refresh_token) {
Err(err) => { Err(err) => {
error!("Failed to decode {} refresh_token: {refresh_token}: {err:?}", ip.ip); debug!("Failed to decode {} refresh_token: {refresh_token}", ip.ip);
//err_silent!(format!("Impossible to read refresh_token: {}", err.message())) err_silent!(format!("Impossible to read refresh_token: {}", err.message()))
// If the token failed to decode, it was probably one of the old style tokens that was just a Base64 string.
// We can generate a claim for them for backwards compatibility. Note that the password refresh claims don't
// check expiration or issuer, so they're not included here.
RefreshJwtClaims {
nbf: 0,
exp: 0,
iss: String::new(),
sub: AuthMethod::Password,
device_token: refresh_token.into(),
token: None,
}
} }
Ok(claims) => claims, Ok(claims) => claims,
}; };

View File

@@ -3,7 +3,7 @@ use std::{
fmt, fmt,
process::exit, process::exit,
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, AtomicUsize, Ordering},
LazyLock, RwLock, LazyLock, RwLock,
}, },
}; };
@@ -14,7 +14,7 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
use crate::{ use crate::{
error::Error, error::Error,
util::{get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags}, util::{get_env, get_env_bool, get_web_vault_version, is_valid_email, parse_experimental_client_feature_flags},
}; };
static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| { static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {
@@ -103,6 +103,7 @@ macro_rules! make_config {
struct Inner { struct Inner {
rocket_shutdown_handle: Option<rocket::Shutdown>, rocket_shutdown_handle: Option<rocket::Shutdown>,
revision: usize,
templates: Handlebars<'static>, templates: Handlebars<'static>,
config: ConfigItems, config: ConfigItems,
@@ -322,7 +323,7 @@ macro_rules! make_config {
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct ConfigItems { $($( $name: make_config! {@type $ty, $none_action}, )+)+ } struct ConfigItems { $($( pub $name: make_config! {@type $ty, $none_action}, )+)+ }
#[derive(Serialize)] #[derive(Serialize)]
struct ElementDoc { struct ElementDoc {
@@ -1325,16 +1326,12 @@ fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String {
if embed_images { if embed_images {
"cid:".to_string() "cid:".to_string()
} else { } else {
// normalize base_url format!("{domain}/vw_static/")
let base_url = domain.trim_end_matches('/');
format!("{base_url}/vw_static/")
} }
} }
fn generate_sso_callback_path(domain: &str) -> String { fn generate_sso_callback_path(domain: &str) -> String {
// normalize base_url format!("{domain}/identity/connect/oidc-signin")
let base_url = domain.trim_end_matches('/');
format!("{base_url}/identity/connect/oidc-signin")
} }
/// Generate the correct URL for the icon service. /// Generate the correct URL for the icon service.
@@ -1471,6 +1468,23 @@ pub enum PathType {
RsaKey, RsaKey,
} }
pub struct CachedConfigOperation<T: Clone> {
generator: fn(&Config) -> T,
value_cache: RwLock<Option<T>>,
revision: AtomicUsize,
}
impl<T: Clone> CachedConfigOperation<T> {
#[allow(private_interfaces)]
pub const fn new(generator: fn(&Config) -> T) -> Self {
CachedConfigOperation {
generator,
value_cache: RwLock::new(None),
revision: AtomicUsize::new(0),
}
}
}
impl Config { impl Config {
pub async fn load() -> Result<Self, Error> { pub async fn load() -> Result<Self, Error> {
// Loading from env and file // Loading from env and file
@@ -1490,6 +1504,7 @@ impl Config {
Ok(Config { Ok(Config {
inner: RwLock::new(Inner { inner: RwLock::new(Inner {
rocket_shutdown_handle: None, rocket_shutdown_handle: None,
revision: 1,
templates: load_templates(&config.templates_folder), templates: load_templates(&config.templates_folder),
config, config,
_env, _env,
@@ -1528,6 +1543,7 @@ impl Config {
writer.config = config; writer.config = config;
writer._usr = builder; writer._usr = builder;
writer._overrides = overrides; writer._overrides = overrides;
writer.revision += 1;
} }
//Save to file //Save to file
@@ -1546,6 +1562,51 @@ impl Config {
self.update_config(builder, false).await self.update_config(builder, false).await
} }
pub async fn delete_user_config(&self) -> Result<(), Error> {
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
operator.delete(&CONFIG_FILENAME).await?;
// Empty user config
let usr = ConfigBuilder::default();
// Config now is env + defaults
let config = {
let env = &self.inner.read().unwrap()._env;
env.build()
};
// Save configs
{
let mut writer = self.inner.write().unwrap();
writer.config = config;
writer._usr = usr;
writer._overrides = Vec::new();
writer.revision += 1;
}
Ok(())
}
pub fn cached_operation<T: Clone>(&self, operation: &CachedConfigOperation<T>) -> T {
let config_revision = self.inner.read().unwrap().revision;
let cache_revision = operation.revision.load(Ordering::Relaxed);
// If the current revision matches the cached revision, return the cached value
if cache_revision == config_revision {
let reader = operation.value_cache.read().unwrap();
return reader.as_ref().unwrap().clone();
}
// Otherwise, compute the value, update the cache and revision, and return the new value
let value = (operation.generator)(&CONFIG);
{
let mut writer = operation.value_cache.write().unwrap();
*writer = Some(value.clone());
operation.revision.store(config_revision, Ordering::Relaxed);
}
value
}
/// Tests whether an email's domain is allowed. A domain is allowed if it /// Tests whether an email's domain is allowed. A domain is allowed if it
/// is in signups_domains_whitelist, or if no whitelist is set (so there /// is in signups_domains_whitelist, or if no whitelist is set (so there
/// are no domain restrictions in effect). /// are no domain restrictions in effect).
@@ -1595,33 +1656,10 @@ impl Config {
} }
} }
pub async fn delete_user_config(&self) -> Result<(), Error> {
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
operator.delete(&CONFIG_FILENAME).await?;
// Empty user config
let usr = ConfigBuilder::default();
// Config now is env + defaults
let config = {
let env = &self.inner.read().unwrap()._env;
env.build()
};
// Save configs
{
let mut writer = self.inner.write().unwrap();
writer.config = config;
writer._usr = usr;
writer._overrides = Vec::new();
}
Ok(())
}
pub fn private_rsa_key(&self) -> String { pub fn private_rsa_key(&self) -> String {
format!("{}.pem", self.rsa_key_filename()) format!("{}.pem", self.rsa_key_filename())
} }
pub fn mail_enabled(&self) -> bool { pub fn mail_enabled(&self) -> bool {
let inner = &self.inner.read().unwrap().config; let inner = &self.inner.read().unwrap().config;
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail) inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
@@ -1849,7 +1887,7 @@ fn to_json<'reg, 'rc>(
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then. // Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added. // The default is based upon the version since this feature is added.
static WEB_VAULT_VERSION: LazyLock<semver::Version> = LazyLock::new(|| { static WEB_VAULT_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {
let vault_version = get_active_web_release(); let vault_version = get_web_vault_version();
// Use a single regex capture to extract version components // Use a single regex capture to extract version components
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap(); let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
re.captures(&vault_version) re.captures(&vault_version)

View File

@@ -126,7 +126,7 @@ fn parse_args() {
exit(0); exit(0);
} else if pargs.contains(["-v", "--version"]) { } else if pargs.contains(["-v", "--version"]) {
config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed); config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed);
let web_vault_version = util::get_active_web_release(); let web_vault_version = util::get_web_vault_version();
println!("Vaultwarden {version}"); println!("Vaultwarden {version}");
println!("Web-Vault {web_vault_version}"); println!("Web-Vault {web_vault_version}");
exit(0); exit(0);

View File

@@ -29,7 +29,7 @@ function isValidIp(ip) {
return ipv4Regex.test(ip) || ipv6Regex.test(ip); return ipv4Regex.test(ip) || ipv6Regex.test(ip);
} }
function checkVersions(platform, installed, latest, commit=null, compare_order=0) { function checkVersions(platform, installed, latest, commit=null, pre_release=false) {
if (installed === "-" || latest === "-") { if (installed === "-" || latest === "-") {
document.getElementById(`${platform}-failed`).classList.remove("d-none"); document.getElementById(`${platform}-failed`).classList.remove("d-none");
return; return;
@@ -37,7 +37,7 @@ function checkVersions(platform, installed, latest, commit=null, compare_order=0
// Only check basic versions, no commit revisions // Only check basic versions, no commit revisions
if (commit === null || installed.indexOf("-") === -1) { if (commit === null || installed.indexOf("-") === -1) {
if (platform === "web" && compare_order === 1) { if (platform === "web" && pre_release === true) {
document.getElementById(`${platform}-prerelease`).classList.remove("d-none"); document.getElementById(`${platform}-prerelease`).classList.remove("d-none");
} else if (installed == latest) { } else if (installed == latest) {
document.getElementById(`${platform}-success`).classList.remove("d-none"); document.getElementById(`${platform}-success`).classList.remove("d-none");
@@ -83,7 +83,7 @@ async function generateSupportString(event, dj) {
let supportString = "### Your environment (Generated via diagnostics page)\n\n"; let supportString = "### Your environment (Generated via diagnostics page)\n\n";
supportString += `* Vaultwarden version: v${dj.current_release}\n`; supportString += `* Vaultwarden version: v${dj.current_release}\n`;
supportString += `* Web-vault version: v${dj.active_web_release}\n`; supportString += `* Web-vault version: v${dj.web_vault_version}\n`;
supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`; supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`;
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`; supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
supportString += `* Database type: ${dj.db_type}\n`; supportString += `* Database type: ${dj.db_type}\n`;
@@ -208,9 +208,9 @@ function initVersionCheck(dj) {
} }
checkVersions("server", serverInstalled, serverLatest, serverLatestCommit); checkVersions("server", serverInstalled, serverLatest, serverLatestCommit);
const webInstalled = dj.active_web_release; const webInstalled = dj.web_vault_version;
const webLatest = dj.latest_web_release; const webLatest = dj.latest_web_build;
checkVersions("web", webInstalled, webLatest, null, dj.web_vault_compare); checkVersions("web", webInstalled, webLatest, null, dj.web_vault_pre_release);
} }
function checkDns(dns_resolved) { function checkDns(dns_resolved) {

View File

@@ -27,13 +27,13 @@
<span class="badge bg-info text-dark d-none abbr-badge" id="web-prerelease" title="You seem to be using a pre-release version.">Pre-Release</span> <span class="badge bg-info text-dark d-none abbr-badge" id="web-prerelease" title="You seem to be using a pre-release version.">Pre-Release</span>
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span id="web-installed">{{page_data.active_web_release}}</span> <span id="web-installed">{{page_data.web_vault_version}}</span>
</dd> </dd>
<dt class="col-sm-5">Web Latest <dt class="col-sm-5">Web Latest
<span class="badge bg-secondary d-none abbr-badge" id="web-failed" title="Unable to determine latest version.">Unknown</span> <span class="badge bg-secondary d-none abbr-badge" id="web-failed" title="Unable to determine latest version.">Unknown</span>
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span id="web-latest">{{page_data.latest_web_release}}</span> <span id="web-latest">{{page_data.latest_web_build}}</span>
</dd> </dd>
{{/if}} {{/if}}
{{#unless page_data.web_vault_enabled}} {{#unless page_data.web_vault_enabled}}

View File

@@ -531,7 +531,7 @@ struct WebVaultVersion {
version: String, version: String,
} }
pub fn get_active_web_release() -> String { pub fn get_web_vault_version() -> String {
let version_files = [ let version_files = [
format!("{}/vw-version.json", CONFIG.web_vault_folder()), format!("{}/vw-version.json", CONFIG.web_vault_folder()),
format!("{}/version.json", CONFIG.web_vault_folder()), format!("{}/version.json", CONFIG.web_vault_folder()),