mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-10-23 01:18:09 +03:00
Compare commits
4
Commits
58a980f850
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b71cb7a832 | ||
|
|
f4696877b9 | ||
|
|
296599003c | ||
|
|
970cc49182 |
No files matched your search
+4
-4
@@ -78,7 +78,7 @@
|
||||
# DB_CONNECTION_RETRIES=15
|
||||
|
||||
## Database timeout
|
||||
## Timeout when acquiring database connection
|
||||
## Timeout in seconds when acquiring database connection
|
||||
# DATABASE_TIMEOUT=30
|
||||
|
||||
## Database idle timeout
|
||||
@@ -173,7 +173,7 @@
|
||||
## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start.
|
||||
# EVENT_CLEANUP_SCHEDULE="0 10 0 * * *"
|
||||
## Number of days to retain events stored in the database.
|
||||
## If unset (the default), events are kept indefinitely and the scheduled job is disabled!
|
||||
## If unset (the default) or 0, events are kept indefinitely and the scheduled job is disabled!
|
||||
# EVENTS_DAYS_RETAIN=
|
||||
##
|
||||
## Cron schedule of the job that cleans old auth requests from the auth request.
|
||||
@@ -233,7 +233,7 @@
|
||||
# USER_SEND_LIMIT=
|
||||
|
||||
## Number of days to wait before auto-deleting a trashed item.
|
||||
## If unset (the default), trashed items are not auto-deleted.
|
||||
## If unset (the default) or 0, trashed items are not auto-deleted.
|
||||
## This setting applies globally, so make sure to inform all users of any changes to this setting.
|
||||
# TRASH_AUTO_DELETE_DAYS=
|
||||
|
||||
@@ -463,7 +463,7 @@
|
||||
|
||||
## Allowed connect-src (Know the risks!)
|
||||
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
|
||||
## Allows other domains to URLs which can be loaded using script interfaces like the Forwarded email alias feature
|
||||
## Allows other domains for URLs which can be loaded using script interfaces like the Forwarded email alias feature
|
||||
## This adds the configured value to the 'Content-Security-Policy' headers 'connect-src' value.
|
||||
## Multiple values must be separated with a whitespace. And only HTTPS values are allowed.
|
||||
## Example: "https://my-addy-io.domain.tld https://my-simplelogin.domain.tld"
|
||||
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
# Enable Rust Caching
|
||||
- name: Rust Caching
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
with:
|
||||
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
||||
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
||||
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
|
||||
- name: Bake ${{ matrix.base_image }} containers
|
||||
id: bake_vw
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
|
||||
env:
|
||||
BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}"
|
||||
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
touch "${RUNNER_TEMP}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: digests-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
||||
|
||||
- name: Upload binaries as artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
||||
path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*-${{ matrix.base_image }}
|
||||
|
||||
@@ -50,6 +50,6 @@ jobs:
|
||||
severity: CRITICAL,HIGH
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
@@ -23,4 +23,4 @@ jobs:
|
||||
|
||||
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
|
||||
- name: Spell Check Repo
|
||||
uses: crate-ci/typos@00f422f3b19c57bc6338715ebfe3316d38768461 # v1.50.3
|
||||
uses: crate-ci/typos@7f9d0819ca1c008ade73f4b41fa9d258d48f605b # v1.51.1
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||
uses: zizmorcore/zizmor-action@85b2b9fb8e3325e7a23d7d0a9d7ba5ae0f999e3e # v0.7.1
|
||||
with:
|
||||
# intentionally not scanning the entire repository,
|
||||
# since it contains integration tests.
|
||||
|
||||
@@ -18,7 +18,7 @@ repos:
|
||||
|
||||
# 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: 00f422f3b19c57bc6338715ebfe3316d38768461 # v1.50.3
|
||||
rev: 7f9d0819ca1c008ade73f4b41fa9d258d48f605b # v1.51.1
|
||||
hooks:
|
||||
- id: typos
|
||||
always_run: true
|
||||
|
||||
Generated
+247
-238
File diff suppressed because it is too large.
Load diff
+12
-12
@@ -91,7 +91,7 @@ dashmap = "6.2.1"
|
||||
|
||||
# Async futures
|
||||
futures = "0.3.34"
|
||||
tokio = { version = "1.53.2", features = [
|
||||
tokio = { version = "1.54.1", features = [
|
||||
"fs",
|
||||
"io-util",
|
||||
"net",
|
||||
@@ -100,14 +100,14 @@ tokio = { version = "1.53.2", features = [
|
||||
"signal",
|
||||
"time",
|
||||
] }
|
||||
tokio-util = { version = "0.7.19", features = ["compat"] }
|
||||
tokio-util = { version = "0.7.20", features = ["compat"] }
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.151"
|
||||
serde_json = "1.0.154"
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "2.3.13", features = ["chrono", "r2d2", "numeric"] }
|
||||
diesel = { version = "2.3.14", features = ["chrono", "r2d2", "numeric"] }
|
||||
diesel_migrations = "2.3.2"
|
||||
|
||||
derive_more = { version = "2.1.1", features = [
|
||||
@@ -125,11 +125,11 @@ libsqlite3-sys = { version = "0.38.2", optional = true }
|
||||
# Crypto-related libraries
|
||||
rand = "0.10.3"
|
||||
ring = "0.17.14"
|
||||
rustls = { version = "0.23.45", features = ["ring", "std"], default-features = false }
|
||||
rustls = { version = "0.23.46", features = ["ring", "std"], default-features = false }
|
||||
subtle = "2.6.1"
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "1.27.0", features = ["v4"] }
|
||||
uuid = { version = "1.28.0", features = ["v4"] }
|
||||
|
||||
# Date and time libraries
|
||||
chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] }
|
||||
@@ -215,7 +215,7 @@ bytes = "1.12.1"
|
||||
svg-hush = "0.9.7"
|
||||
|
||||
# Cache function results (Used for version check and favicon fetching)
|
||||
cached = { version = "4.0.1", features = ["async"] }
|
||||
cached = { version = "4.0.3", features = ["async"] }
|
||||
|
||||
# Used for custom short lived cookie jar during favicon extraction
|
||||
cookie = "0.18.2"
|
||||
@@ -257,18 +257,18 @@ rpassword = "7.5.4"
|
||||
grass_compiler = { version = "0.13.4", default-features = false }
|
||||
|
||||
# File are accessed through Apache OpenDAL
|
||||
opendal = { version = "0.59.4", default-features = false, features = ["services-fs"] }
|
||||
opendal-http-transport-reqwest = { version = "0.59.4", default-features = false, features = ["rustls-no-provider"], optional = true }
|
||||
opendal = { version = "0.59.5", default-features = false, features = ["services-fs"] }
|
||||
opendal-http-transport-reqwest = { version = "0.59.5", default-features = false, features = ["rustls-no-provider"], optional = true }
|
||||
|
||||
# For retrieving AWS credentials, including temporary SSO credentials
|
||||
aws-config = { version = "1.12.0", optional = true, default-features = false, features = [
|
||||
aws-config = { version = "1.13.0", optional = true, default-features = false, features = [
|
||||
"behavior-version-latest",
|
||||
"credentials-process",
|
||||
"rt-tokio",
|
||||
"sso",
|
||||
] }
|
||||
aws-credential-types = { version = "1.3.0", optional = true }
|
||||
aws-smithy-runtime-api = { version = "1.19.0", optional = true }
|
||||
aws-credential-types = { version = "1.3.1", optional = true }
|
||||
aws-smithy-runtime-api = { version = "1.19.1", optional = true }
|
||||
http = { version = "1.5.0", optional = true }
|
||||
reqsign-aws-v4 = { version = "3.3.1", optional = true }
|
||||
reqsign-core = { version = "3.3.2", optional = true }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
vault_version: "v2026.8.0"
|
||||
vault_image_digest: "sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a"
|
||||
# Cross Compile Docker Helper Scripts v1.9.0
|
||||
# Cross Compile Docker Helper Scripts v1.10.0
|
||||
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
||||
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
||||
xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707"
|
||||
xx_image_digest: "sha256:40fe21f44bb8cddf1eb1226db190d0a984118f62ee02a91b77d99448d28264a6"
|
||||
# The `rust_version` variable is extracted from `rust-toolchain.toml`
|
||||
# rust_version: x.yy.z # Rust version to be used
|
||||
debian_version: trixie # Debian release name to be used
|
||||
|
||||
@@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:632375471c7c0
|
||||
########################## ALPINE BUILD IMAGES ##########################
|
||||
## 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
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.99.0 AS build_amd64
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.99.0 AS build_arm64
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.99.0 AS build_armv7
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.99.0 AS build_armv6
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.99.1 AS build_amd64
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.99.1 AS build_arm64
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.99.1 AS build_armv7
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.99.1 AS build_armv6
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# hadolint ignore=DL3006
|
||||
|
||||
@@ -32,11 +32,11 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:632375471c7c0
|
||||
########################## Cross Compile Docker Helper Scripts ##########################
|
||||
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
||||
## And these bash scripts do not have any significant difference if at all
|
||||
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx
|
||||
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:40fe21f44bb8cddf1eb1226db190d0a984118f62ee02a91b77d99448d28264a6 AS xx
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# hadolint ignore=DL3006
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.99.0-slim-trixie AS build
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.99.1-slim-trixie AS build
|
||||
# hadolint ignore=DL3067
|
||||
COPY --from=xx / /
|
||||
ARG TARGETARCH
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
quote = "1.0.47"
|
||||
syn = "3.0.6"
|
||||
syn = "3.0.9"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.99.0"
|
||||
channel = "1.99.1"
|
||||
components = [ "rustfmt", "clippy" ]
|
||||
profile = "minimal"
|
||||
+9
-2
@@ -201,12 +201,15 @@ fn post_admin_login(
|
||||
// If the token is invalid, redirect to login page
|
||||
if validate_token(&data.token) {
|
||||
// If the token received is valid, generate JWT and save it as a cookie
|
||||
let claims = generate_admin_claims();
|
||||
let claims = match generate_admin_claims() {
|
||||
Ok(claims) => claims,
|
||||
Err(e) => return Err(AdminResponse::Ok(render_admin_login(Some(e.message()), redirect.as_deref()))),
|
||||
};
|
||||
let jwt = encode_jwt(&claims);
|
||||
|
||||
let cookie = Cookie::build((COOKIE_NAME, jwt))
|
||||
.path(admin_path())
|
||||
.max_age(time::Duration::minutes(CONFIG.admin_session_lifetime()))
|
||||
.max_age(time::Duration::minutes(i64::from(CONFIG.admin_session_lifetime())))
|
||||
.same_site(SameSite::Strict)
|
||||
.http_only(true)
|
||||
.secure(secure.https);
|
||||
@@ -779,6 +782,8 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
|
||||
.into_keys()
|
||||
.collect();
|
||||
|
||||
let invalid_env_vars = crate::util::INVALID_ENV_VARS.lock().unwrap().join(", ");
|
||||
|
||||
let diagnostics_json = json!({
|
||||
"dns_resolved": dns_resolved,
|
||||
"current_release": VERSION,
|
||||
@@ -801,7 +806,9 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
|
||||
"db_version": get_sql_server_version(&conn).await,
|
||||
"admin_url": format!("{}/diagnostics", admin_url()),
|
||||
"overrides": &CONFIG.get_overrides().join(", "),
|
||||
"config_file_status": &CONFIG.get_config_file_status(),
|
||||
"template_overrides": check_template_overrides().join(", "),
|
||||
"invalid_env_vars": invalid_env_vars,
|
||||
"invalid_feature_flags": invalid_feature_flags,
|
||||
"host_arch": env::consts::ARCH,
|
||||
"host_os": env::consts::OS,
|
||||
|
||||
+177
-105
@@ -1,4 +1,4 @@
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use chrono::Utc;
|
||||
use rocket::{Route, serde::json::Json};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -15,8 +15,8 @@ use crate::{
|
||||
db::{
|
||||
DbConn, DbPool,
|
||||
models::{
|
||||
Cipher, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType, Invitation,
|
||||
Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId,
|
||||
Cipher, EMERGENCY_ACCESS_TIMEOUT_RANGE, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus,
|
||||
EmergencyAccessType, Invitation, Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId,
|
||||
},
|
||||
},
|
||||
mail,
|
||||
@@ -136,19 +136,21 @@ async fn post_emergency_access(
|
||||
|
||||
let data: EmergencyAccessUpdateData = data.into_inner();
|
||||
|
||||
let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) else {
|
||||
err!("Invalid emergency access type.")
|
||||
};
|
||||
|
||||
if !(EMERGENCY_ACCESS_TIMEOUT_RANGE).contains(&data.wait_time_days) {
|
||||
err!("Emergency access wait time is out-of-bounds!")
|
||||
}
|
||||
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) {
|
||||
new_type as i32
|
||||
} else {
|
||||
err!("Invalid emergency access type.")
|
||||
};
|
||||
|
||||
emergency_access.atype = new_type;
|
||||
emergency_access.atype = new_type as i32;
|
||||
emergency_access.wait_time_days = data.wait_time_days;
|
||||
if data.key_encrypted.is_some() {
|
||||
emergency_access.key_encrypted = data.key_encrypted;
|
||||
@@ -210,11 +212,11 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, co
|
||||
let email = data.email.to_lowercase();
|
||||
let wait_time_days = data.wait_time_days;
|
||||
|
||||
let emergency_access_status = EmergencyAccessStatus::Invited as i32;
|
||||
if !(EMERGENCY_ACCESS_TIMEOUT_RANGE).contains(&wait_time_days) {
|
||||
err!("Emergency access wait time is out-of-bounds!")
|
||||
}
|
||||
|
||||
let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) {
|
||||
new_type as i32
|
||||
} else {
|
||||
let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) else {
|
||||
err!("Invalid emergency access type.")
|
||||
};
|
||||
|
||||
@@ -260,6 +262,8 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, co
|
||||
err!(format!("Grantee user already invited: {}", &grantee_user.email))
|
||||
}
|
||||
|
||||
let new_type = new_type as i32;
|
||||
let emergency_access_status = EmergencyAccessStatus::Invited as i32;
|
||||
let mut new_emergency_access =
|
||||
EmergencyAccess::new(grantor_user.uuid, grantee_user.email, emergency_access_status, new_type, wait_time_days);
|
||||
new_emergency_access.save(&conn).await?;
|
||||
@@ -292,7 +296,7 @@ async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, conn: DbCon
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::Invited as i32 {
|
||||
err!("The grantee user is already accepted or confirmed to the organization");
|
||||
err!("The grantee user is already accepted or confirmed for emergency access");
|
||||
}
|
||||
|
||||
let Some(email) = emergency_access.email.clone() else {
|
||||
@@ -453,11 +457,24 @@ async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}', not found for user '{}'.", initiating_user.email)
|
||||
)
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 {
|
||||
err!("Emergency access not valid.")
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}', not confirmed for user '{}'.", initiating_user.email)
|
||||
)
|
||||
}
|
||||
|
||||
if !EMERGENCY_ACCESS_TIMEOUT_RANGE.contains(&emergency_access.wait_time_days) {
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}' timeout range '{}' is invalid", emergency_access.wait_time_days)
|
||||
)
|
||||
}
|
||||
|
||||
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
|
||||
@@ -490,11 +507,17 @@ async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}', not found for user '{}'.", headers.user.email)
|
||||
)
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 {
|
||||
err!("Emergency access not valid.")
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}', not initiated for user '{}'.", headers.user.email)
|
||||
)
|
||||
}
|
||||
|
||||
let Some(grantor_user) = User::find_by_uuid(&headers.user.uuid, &conn).await else {
|
||||
@@ -747,52 +770,71 @@ pub async fn emergency_request_timeout_job(pool: DbPool) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(conn) = pool.get().await {
|
||||
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
|
||||
let Ok(conn) = pool.get().await else {
|
||||
error!("Failed to get DB connection for emergency time-outs!");
|
||||
return;
|
||||
};
|
||||
|
||||
if emergency_access_list.is_empty() {
|
||||
debug!("No emergency request timeout to approve");
|
||||
}
|
||||
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
for mut emer in emergency_access_list {
|
||||
// The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)
|
||||
let recovery_allowed_at =
|
||||
emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days)).unwrap();
|
||||
if recovery_allowed_at.le(&now) {
|
||||
// Only update the access status
|
||||
// Updating the whole record could cause issues when the emergency_notification_reminder_job is also active
|
||||
emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &conn)
|
||||
.await
|
||||
.expect("Unable to update emergency access status");
|
||||
if emergency_access_list.is_empty() {
|
||||
debug!("No emergency request timeout to approve");
|
||||
return;
|
||||
}
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
// get grantor user to send Accepted email
|
||||
let grantor_user =
|
||||
User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found");
|
||||
|
||||
// get grantee user to send Accepted email
|
||||
let grantee_user =
|
||||
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &conn)
|
||||
.await
|
||||
.expect("Grantee user not found");
|
||||
|
||||
mail::send_emergency_access_recovery_timed_out(
|
||||
&grantor_user.email,
|
||||
&grantee_user.name,
|
||||
emer.get_type_as_str(),
|
||||
)
|
||||
.await
|
||||
.expect("Error on sending email");
|
||||
|
||||
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name)
|
||||
.await
|
||||
.expect("Error on sending email");
|
||||
}
|
||||
let now = Utc::now().naive_utc();
|
||||
for mut emer in emergency_access_list {
|
||||
match emer.is_recovery_allowed(&now) {
|
||||
Ok(true) => {} // Recovery is allowed
|
||||
Ok(false) => continue,
|
||||
Err(e) => {
|
||||
error!("Failed to check if recovery is allowed for '{}': {e}", emer.uuid);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get grantor user to send Accepted email
|
||||
let Some(grantor_user) = User::find_by_uuid(&emer.grantor_uuid, &conn).await else {
|
||||
error!("Grantor user '{}' not found for emergency access '{}'", emer.grantor_uuid, emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(grantee_uuid) = emer.grantee_uuid.as_ref() else {
|
||||
error!("Missing grantee for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get grantee user to send Accepted email
|
||||
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
|
||||
error!("Grantee user '{grantee_uuid}' not found for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only update the access status
|
||||
// Updating the whole record could cause issues when the emergency_notification_reminder_job is also active
|
||||
let status_initiated = EmergencyAccessStatus::RecoveryInitiated as i32;
|
||||
let status_approved = EmergencyAccessStatus::RecoveryApproved as i32;
|
||||
if let Err(e) = emer.update_access_status_and_save(status_initiated, status_approved, &now, &conn).await {
|
||||
error!("Unable to update emergency access status for '{}': {e}", emer.uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
if let Err(e) = mail::send_emergency_access_recovery_timed_out(
|
||||
&grantor_user.email,
|
||||
&grantee_user.name,
|
||||
emer.get_type_as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to send emergency access recovery time-out email for '{}': {e}", emer.uuid);
|
||||
}
|
||||
|
||||
if let Err(e) = mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name).await
|
||||
{
|
||||
error!("Failed to send emergency access recovery approved email for '{}': {e}", emer.uuid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Failed to get DB connection while searching emergency request timed out");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,55 +844,85 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(conn) = pool.get().await {
|
||||
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
|
||||
let Ok(conn) = pool.get().await else {
|
||||
error!("Failed to get DB connection for emergency reminders!");
|
||||
return;
|
||||
};
|
||||
|
||||
if emergency_access_list.is_empty() {
|
||||
debug!("No emergency request reminder notification to send");
|
||||
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
|
||||
|
||||
if emergency_access_list.is_empty() {
|
||||
debug!("No emergency request reminder notification to send");
|
||||
return;
|
||||
}
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
for mut emer in emergency_access_list {
|
||||
let Ok(deadline_at) = emer.deadline_at() else {
|
||||
error!("Invalid deadline for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(final_reminder_at) = deadline_at.checked_sub_days(chrono::Days::new(1)) else {
|
||||
error!("Cannot calculate reminder date for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only send reminders during the last 24 hours, else continue
|
||||
if now < final_reminder_at || now >= deadline_at {
|
||||
continue;
|
||||
}
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
for mut emer in emergency_access_list {
|
||||
// The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)
|
||||
// Calculate the day before the recovery will become active
|
||||
let final_recovery_reminder_at =
|
||||
emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days - 1)).unwrap();
|
||||
// Calculate if a day has passed since the previous notification, else no notification has been sent before
|
||||
let next_recovery_reminder_at = if let Some(last_notification_at) = emer.last_notification_at {
|
||||
last_notification_at + TimeDelta::try_days(1).unwrap()
|
||||
} else {
|
||||
now
|
||||
};
|
||||
if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) {
|
||||
// Only update the last notification date
|
||||
// Updating the whole record could cause issues when the emergency_request_timeout_job is also active
|
||||
emer.update_last_notification_date_and_save(&now, &conn)
|
||||
.await
|
||||
.expect("Unable to update emergency access notification date");
|
||||
// Check if we did not sent a recovery mail in the past 24h already, else skip it.
|
||||
// In case something goes wrong with the date calculation or `last_notification_at` is None we use now.
|
||||
// This is to prevent possible missed reminders, this might cause an extra reminder,
|
||||
// but that is better then no reminder at all.
|
||||
let next_reminder_at = if let Some(last_notification_at) = emer.last_notification_at {
|
||||
last_notification_at.checked_add_days(chrono::Days::new(1)).unwrap_or(now)
|
||||
} else {
|
||||
now
|
||||
};
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
// get grantor user to send Accepted email
|
||||
let grantor_user =
|
||||
User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found");
|
||||
|
||||
// get grantee user to send Accepted email
|
||||
let grantee_user =
|
||||
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &conn)
|
||||
.await
|
||||
.expect("Grantee user not found");
|
||||
|
||||
mail::send_emergency_access_recovery_reminder(
|
||||
&grantor_user.email,
|
||||
&grantee_user.name,
|
||||
emer.get_type_as_str(),
|
||||
"1", // This notification is only triggered one day before the activation
|
||||
)
|
||||
.await
|
||||
.expect("Error on sending email");
|
||||
}
|
||||
}
|
||||
// Only send a reminder if the `next_reminder_at` is less then or equal to `now`
|
||||
// Else we can skip the current emergency access this run.
|
||||
if next_reminder_at.gt(&now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get grantor user to send Reminder email
|
||||
let Some(grantor_user) = User::find_by_uuid(&emer.grantor_uuid, &conn).await else {
|
||||
error!("Grantor user '{}' not found for emergency access '{}'", emer.grantor_uuid, emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(grantee_uuid) = emer.grantee_uuid.as_ref() else {
|
||||
error!("Missing grantee for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get grantee user to send Reminder email
|
||||
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
|
||||
error!("Grantee user '{grantee_uuid}' not found for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only update the last notification date
|
||||
// Updating the whole record could cause issues when the emergency_request_timeout_job is also active
|
||||
if let Err(e) = emer.update_last_notification_date_and_save(&now, &conn).await {
|
||||
error!("Unable to update emergency access reminder date for '{}': {e}", emer.uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
if CONFIG.mail_enabled()
|
||||
&& let Err(e) = mail::send_emergency_access_recovery_reminder(
|
||||
&grantor_user.email,
|
||||
&grantee_user.name,
|
||||
emer.get_type_as_str(),
|
||||
"1", // This notification is only triggered one day before the activation
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to send emergency access reminder email for '{}': {e}", emer.uuid);
|
||||
}
|
||||
} else {
|
||||
error!("Failed to get DB connection while searching emergency notification reminder");
|
||||
}
|
||||
}
|
||||
@@ -347,14 +347,17 @@ async fn log_event_impl(
|
||||
|
||||
pub async fn event_cleanup_job(pool: DbPool) {
|
||||
debug!("Start events cleanup job");
|
||||
if CONFIG.events_days_retain().is_none() {
|
||||
debug!("events_days_retain is not configured, abort");
|
||||
if CONFIG.events_days_retain().is_none_or(|d| d == 0) {
|
||||
debug!("events_days_retain is not configured or set to 0, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(conn) = pool.get().await {
|
||||
Event::clean_events(&conn).await.ok();
|
||||
} else {
|
||||
let Ok(conn) = pool.get().await else {
|
||||
error!("Failed to get DB connection while trying to cleanup the events table");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = Event::clean_events(&conn).await {
|
||||
error!("Failed to cleanup the events table: {e}");
|
||||
}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ pub async fn validate_email_code_str(
|
||||
|
||||
if !crypto::ct_eq(issued_token, token) {
|
||||
email_data.add_attempt();
|
||||
if email_data.attempts >= CONFIG.email_attempts_limit() {
|
||||
if email_data.attempts >= u64::from(CONFIG.email_attempts_limit()) {
|
||||
email_data.reset_token();
|
||||
}
|
||||
twofactor.data = email_data.to_json();
|
||||
@@ -313,7 +313,7 @@ pub async fn validate_email_code_str(
|
||||
twofactor.save(conn).await?;
|
||||
|
||||
let dt = DateTime::from_timestamp(email_data.token_sent, 0).expect("Email token timestamp invalid.").naive_utc();
|
||||
let max_time = CONFIG.email_expiration_time().cast_signed();
|
||||
let max_time = i64::from(CONFIG.email_expiration_time());
|
||||
if dt + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() {
|
||||
err!(
|
||||
"Token has expired",
|
||||
|
||||
@@ -196,7 +196,7 @@ pub async fn enforce_2fa_policy_for_org(
|
||||
pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
|
||||
debug!("Sending notifications for incomplete 2FA logins");
|
||||
|
||||
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||
if CONFIG.incomplete_2fa_time_limit() == 0 || !CONFIG.mail_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,11 +206,21 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
|
||||
};
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
let time_limit = TimeDelta::try_minutes(CONFIG.incomplete_2fa_time_limit()).unwrap();
|
||||
let time_before = now - time_limit;
|
||||
let Some(time_limit) = TimeDelta::try_minutes(i64::from(CONFIG.incomplete_2fa_time_limit())) else {
|
||||
error!("Failed to calculate the time limit for sending incomplete 2fa attempts");
|
||||
return;
|
||||
};
|
||||
let Some(time_before) = now.checked_sub_signed(time_limit) else {
|
||||
error!("Failed to calculate the timestamp for sending incomplete 2fa attempts");
|
||||
return;
|
||||
};
|
||||
|
||||
let incomplete_logins = TwoFactorIncomplete::find_logins_before(&time_before, &conn).await;
|
||||
for login in incomplete_logins {
|
||||
let user = User::find_by_uuid(&login.user_uuid, &conn).await.expect("User not found");
|
||||
let Some(user) = User::find_by_uuid(&login.user_uuid, &conn).await else {
|
||||
error!("User '{}', not found!", login.user_uuid);
|
||||
continue;
|
||||
};
|
||||
info!(
|
||||
"User {} did not complete a 2FA login within the configured time limit. IP: {}",
|
||||
user.email, login.ip_address
|
||||
|
||||
@@ -128,23 +128,24 @@ pub async fn validate_protected_action_otp(
|
||||
.await
|
||||
.map_res("Protected action token not found, try sending the code again or restart the process")?;
|
||||
let mut pa_data = ProtectedActionData::from_json(&pa.data)?;
|
||||
pa_data.add_attempt();
|
||||
pa.data = pa_data.to_json();
|
||||
|
||||
// Fail after x attempts if the token has been used too many times.
|
||||
// Don't delete it, as we use it to keep track of attempts.
|
||||
if pa_data.attempts >= CONFIG.email_attempts_limit() {
|
||||
if pa_data.attempts >= u64::from(CONFIG.email_attempts_limit()) {
|
||||
err!("Token has expired")
|
||||
}
|
||||
|
||||
// Check if the token has expired (Using the email 2fa expiration time)
|
||||
let max_time = CONFIG.email_expiration_time().cast_signed();
|
||||
let max_time = i64::from(CONFIG.email_expiration_time());
|
||||
if pa_data.time_since_sent().num_seconds() > max_time {
|
||||
pa.delete(conn).await?;
|
||||
err!("Token has expired")
|
||||
}
|
||||
|
||||
if !crypto::ct_eq(&pa_data.token, otp) {
|
||||
pa_data.add_attempt();
|
||||
pa.data = pa_data.to_json();
|
||||
|
||||
pa.save(conn).await?;
|
||||
err!("Token is invalid")
|
||||
}
|
||||
|
||||
+4
-4
@@ -62,7 +62,7 @@ static CLIENT: LazyLock<Client> = LazyLock::new(|| {
|
||||
// Generate the cookie store
|
||||
let cookie_store = Arc::new(Jar::default());
|
||||
|
||||
let icon_download_timeout = Duration::from_secs(CONFIG.icon_download_timeout());
|
||||
let icon_download_timeout = Duration::from_secs(u64::from(CONFIG.icon_download_timeout()));
|
||||
let pool_idle_timeout = Duration::from_secs(10);
|
||||
// Reuse the client between requests
|
||||
get_reqwest_client_builder(true)
|
||||
@@ -193,14 +193,14 @@ async fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Error> {
|
||||
async fn file_is_expired(path: &str, ttl: u32) -> Result<bool, Error> {
|
||||
let operator = CONFIG.opendal_operator_for_path_type(&PathType::IconCache)?;
|
||||
let meta = operator.stat(path).await?;
|
||||
let modified =
|
||||
meta.last_modified().ok_or_else(|| std::io::Error::other(format!("No last modified time for `{path}`")))?;
|
||||
let age = SystemTime::now().duration_since(modified.into())?;
|
||||
|
||||
Ok(ttl > 0 && ttl <= age.as_secs())
|
||||
Ok(ttl > 0 && u64::from(ttl) <= age.as_secs())
|
||||
}
|
||||
|
||||
async fn icon_is_negcached(path: &str) -> bool {
|
||||
@@ -524,7 +524,7 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
||||
Ok(())
|
||||
}) {
|
||||
Ok(_) => {
|
||||
// Also check if the size is atleast 67 bytes, which seems to be the smallest png i could create
|
||||
// Also check if the size is at least 67 bytes, which seems to be the smallest png I could create
|
||||
if body.len() >= 67 {
|
||||
// Check if the icon type is allowed, else try an icon from the list.
|
||||
icon_type = get_icon_type(&body);
|
||||
|
||||
+2
-2
@@ -447,9 +447,9 @@ async fn password_login(data: ConnectData, user_id: &mut Option<UserId>, conn: &
|
||||
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
||||
if user.last_verifying_at.is_none()
|
||||
|| now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()
|
||||
> CONFIG.signups_verify_resend_time().cast_signed()
|
||||
> i64::from(CONFIG.signups_verify_resend_time())
|
||||
{
|
||||
let resend_limit = CONFIG.signups_verify_resend_limit().cast_signed();
|
||||
let resend_limit = i32::from(CONFIG.signups_verify_resend_limit());
|
||||
if resend_limit == 0 || user.login_verify_count < resend_limit {
|
||||
// We want to send another email verification if we require signups to verify
|
||||
// their email address, and we haven't sent them a reminder in a while...
|
||||
|
||||
+42
-24
@@ -314,6 +314,17 @@ impl LoginJwtClaims {
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_invite_expire_hours(now: &DateTime<Utc>) -> Result<DateTime<Utc>, Error> {
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
let Some(hours_delta) = TimeDelta::try_hours(expire_hours) else {
|
||||
err!("Unable to calculate time delta for invite expire hours. Check `INVITATION_EXPIRATION_HOURS`.");
|
||||
};
|
||||
let Some(exp) = now.checked_add_signed(hours_delta) else {
|
||||
err!("Unable to calculate invitation expire date. Check `INVITATION_EXPIRATION_HOURS`.");
|
||||
};
|
||||
Ok(exp)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InviteJwtClaims {
|
||||
// Not before
|
||||
@@ -337,19 +348,19 @@ pub fn generate_invite_claims(
|
||||
org_id: OrganizationId,
|
||||
member_id: MembershipId,
|
||||
invited_by_email: Option<String>,
|
||||
) -> InviteJwtClaims {
|
||||
) -> Result<InviteJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
InviteJwtClaims {
|
||||
let exp = calculate_invite_expire_hours(&time_now)?;
|
||||
Ok(InviteJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_INVITE_ISSUER.to_string(),
|
||||
sub: user_id,
|
||||
email,
|
||||
org_id,
|
||||
member_id,
|
||||
invited_by_email,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -375,19 +386,19 @@ pub fn generate_emergency_access_invite_claims(
|
||||
emer_id: EmergencyAccessId,
|
||||
grantor_name: String,
|
||||
grantor_email: String,
|
||||
) -> EmergencyAccessInviteJwtClaims {
|
||||
) -> Result<EmergencyAccessInviteJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
EmergencyAccessInviteJwtClaims {
|
||||
let exp = calculate_invite_expire_hours(&time_now)?;
|
||||
Ok(EmergencyAccessInviteJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(),
|
||||
sub: user_id,
|
||||
email,
|
||||
emer_id,
|
||||
grantor_name,
|
||||
grantor_email,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -521,36 +532,43 @@ impl BasicJwtClaims {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {
|
||||
pub fn generate_delete_claims(uuid: String) -> Result<BasicJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
BasicJwtClaims {
|
||||
let exp = calculate_invite_expire_hours(&time_now)?;
|
||||
Ok(BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_DELETE_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_verify_email_claims(user_id: &UserId) -> BasicJwtClaims {
|
||||
pub fn generate_verify_email_claims(user_id: &UserId) -> Result<BasicJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
BasicJwtClaims {
|
||||
let exp = calculate_invite_expire_hours(&time_now)?;
|
||||
Ok(BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
|
||||
sub: user_id.to_string(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_admin_claims() -> BasicJwtClaims {
|
||||
pub fn generate_admin_claims() -> Result<BasicJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
BasicJwtClaims {
|
||||
let lifetime_minutes = i64::from(CONFIG.admin_session_lifetime());
|
||||
let Some(minutes_delta) = TimeDelta::try_minutes(lifetime_minutes) else {
|
||||
err!("Unable to calculate time delta for admin session lifetime. Check `ADMIN_SESSION_LIFETIME`.");
|
||||
};
|
||||
let Some(exp) = time_now.checked_add_signed(minutes_delta) else {
|
||||
err!("Unable to calculate admin session lifetime. Check `ADMIN_SESSION_LIFETIME`.");
|
||||
};
|
||||
Ok(BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_minutes(CONFIG.admin_session_lifetime()).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_ADMIN_ISSUER.to_string(),
|
||||
sub: "admin_panel".to_owned(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_send_claims(send_id: &SendId, file_id: &SendFileId) -> BasicJwtClaims {
|
||||
|
||||
+110
-36
@@ -111,6 +111,7 @@ macro_rules! make_config {
|
||||
_usr: ConfigBuilder,
|
||||
|
||||
_overrides: Vec<&'static str>,
|
||||
_config_file_status: ConfigFileStatus,
|
||||
}
|
||||
|
||||
// Custom Deserialize for ConfigBuilder, mainly based upon https://serde.rs/deserialize-struct.html
|
||||
@@ -259,11 +260,18 @@ macro_rules! make_config {
|
||||
builder
|
||||
}
|
||||
|
||||
async fn from_file() -> Result<Self, Error> {
|
||||
async fn from_file() -> Result<Option<Self>, Error> {
|
||||
let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
||||
let config_bytes = operator.read(&CONFIG_FILENAME).await?;
|
||||
let config_bytes = match operator.read(&CONFIG_FILENAME).await {
|
||||
Ok(bytes) => bytes,
|
||||
// No config file is fine, should not return any error
|
||||
Err(e) if e.kind() == opendal::ErrorKind::NotFound => return Ok(None),
|
||||
// Any other error might be an issue and should be passed back to the caller
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let builder = serde_json::from_slice(&config_bytes.to_vec())?;
|
||||
println!("[INFO] Using saved config from `{}` for configuration.\n", *CONFIG_FILE);
|
||||
serde_json::from_slice(&config_bytes.to_vec()).map_err(Into::into)
|
||||
Ok(Some(builder))
|
||||
}
|
||||
|
||||
fn clear_non_editable(&mut self) {
|
||||
@@ -440,6 +448,8 @@ macro_rules! make_config {
|
||||
"domain_origin",
|
||||
"domain_path",
|
||||
"domain",
|
||||
"duo_ikey",
|
||||
"duo_host",
|
||||
"helo_name",
|
||||
"org_creation_users",
|
||||
"signups_domains_whitelist",
|
||||
@@ -451,6 +461,8 @@ macro_rules! make_config {
|
||||
"sso_authority",
|
||||
"sso_callback_path",
|
||||
"sso_client_id",
|
||||
"yubico_client_id",
|
||||
"yubico_server",
|
||||
];
|
||||
|
||||
let cfg = {
|
||||
@@ -483,6 +495,10 @@ macro_rules! make_config {
|
||||
};
|
||||
overrides
|
||||
}
|
||||
|
||||
pub fn get_config_file_status(&self) -> ConfigFileStatus {
|
||||
self.inner.read().unwrap()._config_file_status
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -539,7 +555,7 @@ make_config! {
|
||||
jobs {
|
||||
/// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.
|
||||
/// Set to 0 to globally disable scheduled jobs.
|
||||
job_poll_interval_ms: u64, false, def, 30_000;
|
||||
job_poll_interval_ms: u32, false, def, 30_000;
|
||||
/// Send purge schedule |> Cron schedule of the job that checks for Sends past their deletion date.
|
||||
/// Defaults to hourly. Set blank to disable this job.
|
||||
send_purge_schedule: String, false, def, "0 5 * * * *".to_owned();
|
||||
@@ -595,19 +611,19 @@ make_config! {
|
||||
/// Per-organization attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per org. When this limit is reached, org members will not be allowed to upload further attachments for ciphers owned by that org.
|
||||
org_attachment_limit: i64, true, option;
|
||||
/// Per-user send storage limit (KB) |> Max kilobytes of sends storage allowed per user. When this limit is reached, the user will not be allowed to upload further sends.
|
||||
user_send_limit: i64, true, option;
|
||||
user_send_limit: i64, true, option;
|
||||
|
||||
/// Trash auto-delete days |> Number of days to wait before auto-deleting a trashed item.
|
||||
/// If unset, trashed items are not auto-deleted. This setting applies globally, so make
|
||||
/// If unset or 0, trashed items are not auto-deleted. This setting applies globally, so make
|
||||
/// sure to inform all users of any changes to this setting.
|
||||
trash_auto_delete_days: i64, true, option;
|
||||
trash_auto_delete_days: u16, true, option;
|
||||
|
||||
/// Incomplete 2FA time limit |> Number of minutes to wait before a 2FA-enabled login is
|
||||
/// considered incomplete, resulting in an email notification. An incomplete 2FA login is one
|
||||
/// where the correct master password was provided but the required 2FA step was not completed,
|
||||
/// which potentially indicates a master password compromise. Set to 0 to disable this check.
|
||||
/// This setting applies globally to all users.
|
||||
incomplete_2fa_time_limit: i64, true, def, 3;
|
||||
incomplete_2fa_time_limit: u16, true, def, 3;
|
||||
|
||||
/// Disable icon downloads |> Set to true to disable icon downloading in the internal icon service.
|
||||
/// This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external
|
||||
@@ -620,9 +636,9 @@ make_config! {
|
||||
/// this will prevent logins from succeeding until the address has been verified
|
||||
signups_verify: bool, true, def, false;
|
||||
/// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
|
||||
signups_verify_resend_time: u64, true, def, 3_600;
|
||||
signups_verify_resend_time: u32, true, def, 3_600;
|
||||
/// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)
|
||||
signups_verify_resend_limit: u32, true, def, 6;
|
||||
signups_verify_resend_limit: u16, true, def, 6;
|
||||
/// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled
|
||||
signups_domains_whitelist: String, true, def, String::new();
|
||||
/// Enable event logging |> Enables event logging for organizations.
|
||||
@@ -634,7 +650,7 @@ make_config! {
|
||||
invitations_allowed: bool, true, def, true;
|
||||
/// Invitation token expiration time (in hours) |> The number of hours after which an organization invite token, emergency access invite token,
|
||||
/// email verification token and deletion request token will expire (must be at least 1)
|
||||
invitation_expiration_hours: u32, false, def, 120;
|
||||
invitation_expiration_hours: u16, false, def, 120;
|
||||
/// Enable emergency access |> Controls whether users can enable emergency access to their accounts. This setting applies globally to all users.
|
||||
emergency_access_allowed: bool, true, def, true;
|
||||
/// Allow email change |> Controls whether users can change their email. This setting applies globally to all users.
|
||||
@@ -655,8 +671,8 @@ make_config! {
|
||||
/// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
|
||||
invitation_org_name: String, true, def, "Vaultwarden".to_owned();
|
||||
|
||||
/// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefinitely.
|
||||
events_days_retain: i64, false, option;
|
||||
/// Events days retain |> Number of days to retain events stored in the database. If unset or 0, events are kept indefinitely.
|
||||
events_days_retain: u16, false, option;
|
||||
},
|
||||
|
||||
client {
|
||||
@@ -694,13 +710,13 @@ make_config! {
|
||||
/// Temporary redirects are useful while testing different icon services, but once a service
|
||||
/// has been decided on, consider using permanent redirects for cacheability. The legacy codes
|
||||
/// are currently better supported by the Bitwarden clients.
|
||||
icon_redirect_code: u32, true, def, 302;
|
||||
icon_redirect_code: u16, true, def, 302;
|
||||
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be refreshed
|
||||
icon_cache_ttl: u64, true, def, 2_592_000;
|
||||
icon_cache_ttl: u32, true, def, 2_592_000;
|
||||
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
|
||||
icon_cache_negttl: u64, true, def, 259_200;
|
||||
icon_cache_negttl: u32, true, def, 259_200;
|
||||
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
||||
icon_download_timeout: u64, true, def, 10;
|
||||
icon_download_timeout: u16, true, def, 10;
|
||||
|
||||
/// [Deprecated] Icon blacklist Regex |> Use `http_request_block_regex` instead
|
||||
icon_blacklist_regex: String, false, option;
|
||||
@@ -751,11 +767,11 @@ make_config! {
|
||||
/// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely
|
||||
db_connection_retries: u32, false, def, 15;
|
||||
|
||||
/// Timeout when acquiring database connection
|
||||
database_timeout: u64, false, def, 30;
|
||||
/// Timeout in seconds when acquiring database connection
|
||||
database_timeout: u32, false, def, 30;
|
||||
|
||||
/// Timeout in seconds before idle connections to the database are closed
|
||||
database_idle_timeout: u64, false, def, 600;
|
||||
database_idle_timeout: u32, false, def, 600;
|
||||
|
||||
/// Database connection max pool size
|
||||
database_max_conns: u32, false, def, 10;
|
||||
@@ -772,26 +788,26 @@ make_config! {
|
||||
/// Allowed iframe ancestors (Know the risks!) |> Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets
|
||||
allowed_iframe_ancestors: String, true, def, String::new();
|
||||
|
||||
/// Allowed connect-src (Know the risks!) |> Allows other domains to URLs which can be loaded using script interfaces like the Forwarded email alias feature
|
||||
/// Allowed connect-src (Know the risks!) |> Allows other domains for URLs which can be loaded using script interfaces like the Forwarded email alias feature
|
||||
allowed_connect_src: String, true, def, String::new();
|
||||
|
||||
/// Seconds between login requests |> Number of seconds, on average, between login and 2FA requests from the same IP address before rate limiting kicks in
|
||||
login_ratelimit_seconds: u64, false, def, 60;
|
||||
login_ratelimit_seconds: u16, false, def, 60;
|
||||
/// Max burst size for login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `login_ratelimit_seconds`. Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2
|
||||
login_ratelimit_max_burst: u32, false, def, 10;
|
||||
login_ratelimit_max_burst: u16, false, def, 10;
|
||||
|
||||
/// Seconds between unauthenticated requests |> Number of seconds, on average, between requests from the same IP address to any of the rate limited unauthenticated endpoints
|
||||
unauthenticated_ratelimit_seconds: u64, false, def, 60;
|
||||
unauthenticated_ratelimit_seconds: u16, false, def, 60;
|
||||
/// Max burst size for unauthenticated requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `unauthenticated_ratelimit_seconds`. This is shared between several endpoints, so it needs to be more lenient than the login one
|
||||
unauthenticated_ratelimit_max_burst: u32, false, def, 50;
|
||||
unauthenticated_ratelimit_max_burst: u16, false, def, 50;
|
||||
|
||||
/// Seconds between admin login requests |> Number of seconds, on average, between admin requests from the same IP address before rate limiting kicks in
|
||||
admin_ratelimit_seconds: u64, false, def, 300;
|
||||
admin_ratelimit_seconds: u16, false, def, 300;
|
||||
/// Max burst size for admin login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `admin_ratelimit_seconds`
|
||||
admin_ratelimit_max_burst: u32, false, def, 3;
|
||||
admin_ratelimit_max_burst: u16, false, def, 3;
|
||||
|
||||
/// Admin session lifetime |> Set the lifetime of admin sessions to this value (in minutes).
|
||||
admin_session_lifetime: i64, true, def, 20;
|
||||
admin_session_lifetime: u32, true, def, 20;
|
||||
|
||||
/// Enable groups (BETA!) (Know the risks!) |> Enables groups support for organizations (Currently contains known issues!).
|
||||
org_groups_enabled: bool, false, def, false;
|
||||
@@ -844,8 +860,8 @@ make_config! {
|
||||
sso_master_password_policy: String, true, option;
|
||||
/// Use SSO only for auth not the session lifecycle |> Use default Vaultwarden session lifecycle (Idle refresh token valid for 30days)
|
||||
sso_auth_only_not_session: bool, true, def, false;
|
||||
/// Client cache for discovery endpoint. |> Duration in seconds (0 or less to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache
|
||||
sso_client_cache_expiration: u64, true, def, 0;
|
||||
/// Client cache for discovery endpoint. |> Duration in seconds (0 to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache
|
||||
sso_client_cache_expiration: u32, true, def, 0;
|
||||
/// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required
|
||||
sso_debug_tokens: bool, true, def, false;
|
||||
},
|
||||
@@ -903,11 +919,11 @@ make_config! {
|
||||
/// SMTP Auth mechanism |> Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. Possible values: ["Plain", "Login", "Xoauth2"]. Multiple options need to be separated by a comma ','.
|
||||
smtp_auth_mechanism: String, true, option;
|
||||
/// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
|
||||
smtp_timeout: u64, true, def, 15;
|
||||
smtp_timeout: u16, true, def, 15;
|
||||
/// Server name sent during HELO |> By default this value should be the machine's hostname, but might need to be changed in case it trips some anti-spam filters
|
||||
helo_name: String, true, option;
|
||||
/// Embed images as email attachments.
|
||||
smtp_embed_images: bool, true, def, true;
|
||||
smtp_embed_images: bool, true, def, true;
|
||||
/// _smtp_img_src
|
||||
_smtp_img_src: String, false, generated, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain);
|
||||
/// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
|
||||
@@ -925,9 +941,9 @@ make_config! {
|
||||
/// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting.
|
||||
email_token_size: u8, true, def, 6;
|
||||
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
||||
email_expiration_time: u64, true, def, 600;
|
||||
email_expiration_time: u32, true, def, 600;
|
||||
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
|
||||
email_attempts_limit: u64, true, def, 3;
|
||||
email_attempts_limit: u8, true, def, 3;
|
||||
/// Setup email 2FA at signup |> Setup email 2FA provider on registration regardless of any organization policy
|
||||
email_2fa_enforce_on_verified_invite: bool, true, def, false;
|
||||
/// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed
|
||||
@@ -983,7 +999,15 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
|
||||
}
|
||||
|
||||
if cfg.database_min_conns > cfg.database_max_conns {
|
||||
err!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.");
|
||||
err!("`DATABASE_MIN_CONNS` must be less than or equal to `DATABASE_MAX_CONNS`.");
|
||||
}
|
||||
|
||||
if cfg.database_timeout < 1 {
|
||||
err!("`DATABASE_TIMEOUT` must be at least 1 second");
|
||||
}
|
||||
|
||||
if cfg.database_idle_timeout < 1 {
|
||||
err!("`DATABASE_IDLE_TIMEOUT` must be at least 1 second");
|
||||
}
|
||||
|
||||
if let Some(log_file) = &cfg.log_file
|
||||
@@ -1274,6 +1298,30 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
|
||||
err!("`PURGE_INCOMPLETE_SSO_AUTH` is not a valid cron expression")
|
||||
}
|
||||
|
||||
if cfg.login_ratelimit_seconds < 1 {
|
||||
err!("`LOGIN_RATELIMIT_SECONDS` must be at least 1 second");
|
||||
}
|
||||
|
||||
if cfg.login_ratelimit_max_burst < 1 {
|
||||
err!("`LOGIN_RATELIMIT_MAX_BURST` must be at least 1");
|
||||
}
|
||||
|
||||
if cfg.admin_ratelimit_seconds < 1 {
|
||||
err!("`ADMIN_RATELIMIT_SECONDS` must be at least 1 second");
|
||||
}
|
||||
|
||||
if cfg.admin_ratelimit_max_burst < 1 {
|
||||
err!("`ADMIN_RATELIMIT_MAX_BURST` must be at least 1");
|
||||
}
|
||||
|
||||
if cfg.unauthenticated_ratelimit_seconds < 1 {
|
||||
err!("`UNAUTHENTICATED_RATELIMIT_SECONDS` must be at least 1 second");
|
||||
}
|
||||
|
||||
if cfg.unauthenticated_ratelimit_max_burst < 1 {
|
||||
err!("`UNAUTHENTICATED_RATELIMIT_MAX_BURST` must be at least 1");
|
||||
}
|
||||
|
||||
if !cfg.disable_admin_token {
|
||||
match cfg.admin_token.as_ref() {
|
||||
Some(t) if t.starts_with("$argon2") => {
|
||||
@@ -1443,11 +1491,34 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[
|
||||
"pm-32009-new-item-types",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
pub enum ConfigFileStatus {
|
||||
NotFound,
|
||||
Loaded,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn load() -> Result<Self, Error> {
|
||||
// Loading from env and file
|
||||
let env = ConfigBuilder::from_env();
|
||||
let usr = ConfigBuilder::from_file().await.unwrap_or_default();
|
||||
let (usr, config_file_status) = match ConfigBuilder::from_file().await {
|
||||
Ok(Some(usr)) => (usr, ConfigFileStatus::Loaded),
|
||||
Ok(None) => (ConfigBuilder::default(), ConfigFileStatus::NotFound),
|
||||
Err(e) => {
|
||||
println!(
|
||||
"[ERROR] Failed to load configuration from `{}`\n\
|
||||
#####################################################################################\n\
|
||||
# The file seems to exist but might contain invalid JSON or invalid values. #\n\
|
||||
# Check the syntax and make sure every value is valid and within its allowed range. #\n\
|
||||
# This file is not used until those errors are resolved. #\n\
|
||||
#####################################################################################\n\
|
||||
{e:?}\n",
|
||||
*CONFIG_FILE
|
||||
);
|
||||
(ConfigBuilder::default(), ConfigFileStatus::Error)
|
||||
}
|
||||
};
|
||||
|
||||
// Create merged config, config file overwrites env
|
||||
let mut overrides = Vec::new();
|
||||
@@ -1467,6 +1538,7 @@ impl Config {
|
||||
_env: env,
|
||||
_usr: usr,
|
||||
_overrides: overrides,
|
||||
_config_file_status: config_file_status,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -1500,6 +1572,7 @@ impl Config {
|
||||
writer.config = config;
|
||||
writer._usr = builder;
|
||||
writer._overrides = overrides;
|
||||
writer._config_file_status = ConfigFileStatus::Loaded;
|
||||
}
|
||||
|
||||
//Save to file
|
||||
@@ -1591,6 +1664,7 @@ impl Config {
|
||||
writer.config = config;
|
||||
writer._usr = usr;
|
||||
writer._overrides = Vec::new();
|
||||
writer._config_file_status = ConfigFileStatus::NotFound;
|
||||
}
|
||||
|
||||
// Invalidate CSS Cache because several config items might have impact on the rendered CSS
|
||||
|
||||
+3
-3
@@ -212,8 +212,8 @@ impl DbPool {
|
||||
let pool = Pool::builder()
|
||||
.max_size(max_conns)
|
||||
.min_idle(Some(CONFIG.database_min_conns()))
|
||||
.idle_timeout(Some(Duration::from_secs(CONFIG.database_idle_timeout())))
|
||||
.connection_timeout(Duration::from_secs(CONFIG.database_timeout()))
|
||||
.idle_timeout(Some(Duration::from_secs(u64::from(CONFIG.database_idle_timeout()))))
|
||||
.connection_timeout(Duration::from_secs(u64::from(CONFIG.database_timeout())))
|
||||
.connection_customizer(Box::new(DbConnOptions {
|
||||
init_stmts: conn_type.get_init_stmts(),
|
||||
}))
|
||||
@@ -233,7 +233,7 @@ impl DbPool {
|
||||
|
||||
// Get a connection from the pool
|
||||
pub async fn get(&self) -> Result<DbConn, Error> {
|
||||
let duration = Duration::from_secs(CONFIG.database_timeout());
|
||||
let duration = Duration::from_secs(u64::from(CONFIG.database_timeout()));
|
||||
let permit = match timeout(duration, Arc::clone(&self.semaphore).acquire_owned()).await {
|
||||
Ok(p) => p.expect("Semaphore should be open"),
|
||||
Err(_) => {
|
||||
|
||||
@@ -197,7 +197,7 @@ impl AuthRequest {
|
||||
pub async fn purge_expired_auth_requests(conn: &DbConn) {
|
||||
// delete auth requests older than 15 minutes which is functionally equivalent to upstream:
|
||||
// https://github.com/bitwarden/server/blob/f8ee2270409f7a13125cd414c450740af605a175/src/Sql/dbo/Auth/Stored%20Procedures/AuthRequest_DeleteIfExpired.sql
|
||||
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(15).unwrap();
|
||||
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::minutes(15);
|
||||
for auth_request in Self::find_created_before(&expiry_time, conn).await {
|
||||
auth_request.delete(conn).await.ok();
|
||||
}
|
||||
|
||||
+16
-6
@@ -494,12 +494,22 @@ impl Cipher {
|
||||
|
||||
/// Purge all ciphers that are old enough to be auto-deleted.
|
||||
pub async fn purge_trash(conn: &DbConn) {
|
||||
if let Some(auto_delete_days) = CONFIG.trash_auto_delete_days() {
|
||||
let now = Utc::now().naive_utc();
|
||||
let dt = now - TimeDelta::try_days(auto_delete_days).unwrap();
|
||||
for cipher in Self::find_deleted_before(&dt, conn).await {
|
||||
cipher.delete(conn).await.ok();
|
||||
}
|
||||
let Some(auto_delete_days) = CONFIG.trash_auto_delete_days().filter(|&d| d != 0) else {
|
||||
debug!("trash_auto_delete_days is not configured or set to 0, skipping");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(days) = TimeDelta::try_days(i64::from(auto_delete_days)) else {
|
||||
error!("Unable to calculate auto delete days!");
|
||||
return;
|
||||
};
|
||||
let now = Utc::now().naive_utc();
|
||||
let Some(dt) = now.checked_sub_signed(days) else {
|
||||
error!("Unable to calculate trash purge date!");
|
||||
return;
|
||||
};
|
||||
for cipher in Self::find_deleted_before(&dt, conn).await {
|
||||
cipher.delete(conn).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
use diesel::prelude::*;
|
||||
use serde_json::Value;
|
||||
@@ -112,8 +112,26 @@ impl EmergencyAccess {
|
||||
"object": "emergencyAccessGranteeDetails",
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn deadline_at(&self) -> Result<NaiveDateTime, crate::Error> {
|
||||
if !(EMERGENCY_ACCESS_TIMEOUT_RANGE).contains(&self.wait_time_days) {
|
||||
err!("Emergency access wait time is out-of-bounds!")
|
||||
}
|
||||
|
||||
let initiated_at = self.recovery_initiated_at.map_res("Emergency access initiation date missing!")?;
|
||||
|
||||
TimeDelta::try_days(i64::from(self.wait_time_days))
|
||||
.and_then(|w| initiated_at.checked_add_signed(w))
|
||||
.map_res("Emergency access deadline is invalid")
|
||||
}
|
||||
|
||||
pub fn is_recovery_allowed(&self, now: &NaiveDateTime) -> Result<bool, crate::Error> {
|
||||
Ok(self.deadline_at()?.le(now))
|
||||
}
|
||||
}
|
||||
|
||||
pub const EMERGENCY_ACCESS_TIMEOUT_RANGE: std::ops::RangeInclusive<i32> = 1..=90;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum EmergencyAccessType {
|
||||
View = 0,
|
||||
@@ -169,27 +187,42 @@ impl EmergencyAccess {
|
||||
|
||||
pub async fn update_access_status_and_save(
|
||||
&mut self,
|
||||
status: i32,
|
||||
from_status: i32,
|
||||
to_status: i32,
|
||||
date: &NaiveDateTime,
|
||||
conn: &DbConn,
|
||||
) -> EmptyResult {
|
||||
let uuid = self.uuid.clone();
|
||||
let updated_at = *date;
|
||||
let res = conn
|
||||
.run(move |conn| {
|
||||
crate::util::retry(
|
||||
|| {
|
||||
diesel::update(
|
||||
emergency_access::table
|
||||
.filter(emergency_access::uuid.eq(&uuid))
|
||||
.filter(emergency_access::status.eq(from_status)),
|
||||
)
|
||||
.set((emergency_access::status.eq(to_status), emergency_access::updated_at.eq(updated_at)))
|
||||
.execute(conn)
|
||||
},
|
||||
10,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(1) => {} // One record updated, which is valid
|
||||
Ok(_) => err!("Emergency access status was changed already"),
|
||||
Err(e) => err!("Error updating emergency access status", format!("{e}")),
|
||||
}
|
||||
|
||||
self.status = to_status;
|
||||
self.updated_at = updated_at;
|
||||
|
||||
// Update the grantee so that it will refresh it's status.
|
||||
User::update_uuid_revision(self.grantee_uuid.as_ref().expect("Error getting grantee"), conn).await;
|
||||
self.status = status;
|
||||
date.clone_into(&mut self.updated_at);
|
||||
|
||||
conn.run(move |conn| {
|
||||
crate::util::retry(
|
||||
|| {
|
||||
diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid)))
|
||||
.set((emergency_access::status.eq(status), emergency_access::updated_at.eq(date)))
|
||||
.execute(conn)
|
||||
},
|
||||
10,
|
||||
)
|
||||
.map_res("Error updating emergency access status")
|
||||
})
|
||||
.await
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_last_notification_date_and_save(&mut self, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
|
||||
|
||||
+17
-11
@@ -347,17 +347,23 @@ impl Event {
|
||||
}
|
||||
|
||||
pub async fn clean_events(conn: &DbConn) -> EmptyResult {
|
||||
if let Some(days_to_retain) = CONFIG.events_days_retain() {
|
||||
let dt = Utc::now().naive_utc() - TimeDelta::try_days(days_to_retain).unwrap();
|
||||
conn.run(move |conn| {
|
||||
diesel::delete(event::table.filter(event::event_date.lt(dt)))
|
||||
.execute(conn)
|
||||
.map_res("Error cleaning old events")
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
let Some(days_to_retain) = CONFIG.events_days_retain().filter(|&d| d != 0) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(days_to_retain) = TimeDelta::try_days(i64::from(days_to_retain)) else {
|
||||
err!("Unable to calculate the event retention date, check 'EVENTS_DAYS_RETAIN'")
|
||||
};
|
||||
|
||||
let Some(dt) = Utc::now().naive_utc().checked_sub_signed(days_to_retain) else {
|
||||
err!("Unable to calculate the event retention date, check 'EVENTS_DAYS_RETAIN'")
|
||||
};
|
||||
conn.run(move |conn| {
|
||||
diesel::delete(event::table.filter(event::event_date.lt(dt)))
|
||||
.execute(conn)
|
||||
.map_res("Error cleaning old events")
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ pub use self::auth_request::{AuthRequest, AuthRequestId};
|
||||
pub use self::cipher::{Cipher, CipherId, RepromptType};
|
||||
pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};
|
||||
pub use self::device::{Device, DeviceId, DeviceType, DeviceWithAuthRequest, PushId};
|
||||
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType};
|
||||
pub use self::emergency_access::{
|
||||
EMERGENCY_ACCESS_TIMEOUT_RANGE, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType,
|
||||
};
|
||||
pub use self::event::{Event, EventType};
|
||||
pub use self::favorite::Favorite;
|
||||
pub use self::folder::{Folder, FolderCipher, FolderId};
|
||||
|
||||
@@ -37,7 +37,7 @@ impl TwoFactorIncomplete {
|
||||
ip: &ClientIp,
|
||||
conn: &DbConn,
|
||||
) -> EmptyResult {
|
||||
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||
if CONFIG.incomplete_2fa_time_limit() == 0 || !CONFIG.mail_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ impl TwoFactorIncomplete {
|
||||
}
|
||||
|
||||
pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult {
|
||||
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||
if CONFIG.incomplete_2fa_time_limit() == 0 || !CONFIG.mail_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -36,7 +36,7 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||
|
||||
let smtp_client = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(host.as_str())
|
||||
.port(CONFIG.smtp_port())
|
||||
.timeout(Some(Duration::from_secs(CONFIG.smtp_timeout())));
|
||||
.timeout(Some(Duration::from_secs(u64::from(CONFIG.smtp_timeout()))));
|
||||
|
||||
// Determine security
|
||||
let smtp_client = if CONFIG.smtp_security() == *"off" {
|
||||
@@ -169,7 +169,7 @@ pub async fn send_password_hint(address: &str, hint: Option<String>) -> EmptyRes
|
||||
}
|
||||
|
||||
pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult {
|
||||
let claims = generate_delete_claims(user_id.to_string());
|
||||
let claims = generate_delete_claims(user_id.to_string())?;
|
||||
let delete_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -187,7 +187,7 @@ pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult
|
||||
}
|
||||
|
||||
pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult {
|
||||
let claims = generate_verify_email_claims(user_id);
|
||||
let claims = generate_verify_email_claims(user_id)?;
|
||||
let verify_email_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -237,7 +237,7 @@ pub async fn send_welcome(address: &str) -> EmptyResult {
|
||||
}
|
||||
|
||||
pub async fn send_welcome_must_verify(address: &str, user_id: &UserId) -> EmptyResult {
|
||||
let claims = generate_verify_email_claims(user_id);
|
||||
let claims = generate_verify_email_claims(user_id)?;
|
||||
let verify_email_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -292,7 +292,7 @@ pub async fn send_invite(
|
||||
org_id.clone(),
|
||||
member_id.clone(),
|
||||
invited_by_email,
|
||||
);
|
||||
)?;
|
||||
let invite_token = encode_jwt(&claims);
|
||||
let mut query = url::Url::parse("https://query.builder").unwrap();
|
||||
{
|
||||
@@ -351,7 +351,7 @@ pub async fn send_emergency_access_invite(
|
||||
emer_id.clone(),
|
||||
String::from(grantor_name),
|
||||
String::from(grantor_email),
|
||||
);
|
||||
)?;
|
||||
|
||||
// Build the query here to ensure proper escaping
|
||||
let mut query = url::Url::parse("https://query.builder").unwrap();
|
||||
|
||||
+4
-2
@@ -729,7 +729,7 @@ fn schedule_jobs(pool: db::DbPool) {
|
||||
// Cleanup the event table of records x days old.
|
||||
if CONFIG.org_events_enabled()
|
||||
&& !CONFIG.event_cleanup_schedule().is_empty()
|
||||
&& CONFIG.events_days_retain().is_some()
|
||||
&& CONFIG.events_days_retain().is_some_and(|d| d != 0)
|
||||
{
|
||||
sched.add(Job::new(CONFIG.event_cleanup_schedule().parse().unwrap(), || {
|
||||
runtime.spawn(api::event_cleanup_job(pool.clone()));
|
||||
@@ -754,7 +754,9 @@ fn schedule_jobs(pool: db::DbPool) {
|
||||
// tick, the one that was added earlier will run first.
|
||||
loop {
|
||||
sched.tick();
|
||||
runtime.block_on(tokio::time::sleep(tokio::time::Duration::from_millis(CONFIG.job_poll_interval_ms())));
|
||||
runtime.block_on(tokio::time::sleep(tokio::time::Duration::from_millis(u64::from(
|
||||
CONFIG.job_poll_interval_ms(),
|
||||
))));
|
||||
}
|
||||
})
|
||||
.expect("Error spawning job scheduler thread");
|
||||
|
||||
+6
-6
@@ -7,20 +7,20 @@ use crate::{CONFIG, Error};
|
||||
type Limiter<T = IpAddr> = RateLimiter<T, DashMapStateStore<T>, DefaultClock>;
|
||||
|
||||
static LIMITER_LOGIN: LazyLock<Limiter> = LazyLock::new(|| {
|
||||
let seconds = Duration::from_secs(CONFIG.login_ratelimit_seconds());
|
||||
let burst = NonZeroU32::new(CONFIG.login_ratelimit_max_burst()).expect("Non-zero login ratelimit burst");
|
||||
let seconds = Duration::from_secs(u64::from(CONFIG.login_ratelimit_seconds()));
|
||||
let burst = NonZeroU32::new(u32::from(CONFIG.login_ratelimit_max_burst())).expect("Non-zero login ratelimit burst");
|
||||
RateLimiter::keyed(Quota::with_period(seconds).expect("Non-zero login ratelimit seconds").allow_burst(burst))
|
||||
});
|
||||
|
||||
static LIMITER_ADMIN: LazyLock<Limiter> = LazyLock::new(|| {
|
||||
let seconds = Duration::from_secs(CONFIG.admin_ratelimit_seconds());
|
||||
let burst = NonZeroU32::new(CONFIG.admin_ratelimit_max_burst()).expect("Non-zero admin ratelimit burst");
|
||||
let seconds = Duration::from_secs(u64::from(CONFIG.admin_ratelimit_seconds()));
|
||||
let burst = NonZeroU32::new(u32::from(CONFIG.admin_ratelimit_max_burst())).expect("Non-zero admin ratelimit burst");
|
||||
RateLimiter::keyed(Quota::with_period(seconds).expect("Non-zero admin ratelimit seconds").allow_burst(burst))
|
||||
});
|
||||
|
||||
static LIMITER_UNAUTHENTICATED: LazyLock<Limiter> = LazyLock::new(|| {
|
||||
let seconds = Duration::from_secs(CONFIG.unauthenticated_ratelimit_seconds());
|
||||
let burst = NonZeroU32::new(CONFIG.unauthenticated_ratelimit_max_burst())
|
||||
let seconds = Duration::from_secs(u64::from(CONFIG.unauthenticated_ratelimit_seconds()));
|
||||
let burst = NonZeroU32::new(u32::from(CONFIG.unauthenticated_ratelimit_max_burst()))
|
||||
.expect("Non-zero unauthenticated ratelimit burst");
|
||||
RateLimiter::keyed(
|
||||
Quota::with_period(seconds).expect("Non-zero unauthenticated ratelimit seconds").allow_burst(burst),
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ static CLIENT_CACHE_KEY: LazyLock<String> = LazyLock::new(|| "sso-client".to_own
|
||||
static CLIENT_CACHE: LazyLock<moka::sync::Cache<String, Client>> = LazyLock::new(|| {
|
||||
moka::sync::Cache::builder()
|
||||
.max_capacity(1)
|
||||
.time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration()))
|
||||
.time_to_live(Duration::from_secs(u64::from(CONFIG.sso_client_cache_expiration())))
|
||||
.build()
|
||||
});
|
||||
static REFRESH_CACHE: LazyLock<moka::future::Cache<String, Result<RefreshTokenResponse, String>>> =
|
||||
|
||||
+9
-2
@@ -93,7 +93,9 @@ async function generateSupportString(event, dj) {
|
||||
supportString += `* Running within a container: ${yn(dj.running_within_container)} (Base: ${dj.container_base_image})\n`;
|
||||
supportString += `* Database type: ${dj.db_type}\n`;
|
||||
supportString += `* Database version: ${dj.db_version}\n`;
|
||||
supportString += `* Uses config.json: ${yn(dj.overrides !== "")}\n`;
|
||||
supportString += `* Uses config.json: ${yn(dj.config_file_status !== "NotFound")}`;
|
||||
supportString += dj.config_file_status !== "NotFound" ? ` (Valid: ${chk(dj.config_file_status === "Loaded")})\n` : "\n";
|
||||
supportString += `* Environment variables are valid: ${chk(dj.invalid_env_vars == "")}\n`;
|
||||
supportString += `* Uses custom templates: ${yn(dj.template_overrides !== "")}${template_overrides}\n`;
|
||||
supportString += `* Uses a reverse proxy: ${yn(dj.ip_header_exists)}\n`;
|
||||
if (dj.ip_header_exists) {
|
||||
@@ -116,7 +118,7 @@ async function generateSupportString(event, dj) {
|
||||
}
|
||||
supportString += `* HTTP Response Checks: ${chk(httpResponseCheck)}\n`;
|
||||
if (dj.invalid_feature_flags != "") {
|
||||
supportString += "* Invalid feature flags: true\n";
|
||||
supportString += "* Invalid feature flags: true :x:\n";
|
||||
}
|
||||
|
||||
const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, {
|
||||
@@ -132,6 +134,11 @@ async function generateSupportString(event, dj) {
|
||||
supportString += "\n### Config & Details (Generated via diagnostics page)\n\n";
|
||||
supportString += "<details><summary>Show Config & Details</summary>\n";
|
||||
|
||||
// Show invalid environment variables if they exists
|
||||
if (dj.invalid_env_vars != "") {
|
||||
supportString += `\n**Invalid environment variables:** ${dj.invalid_env_vars}\n`;
|
||||
}
|
||||
|
||||
// Add overrides if they exists
|
||||
if (dj.overrides != "") {
|
||||
supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`;
|
||||
|
||||
Vendored
+2
-2
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs5/dt-3.1.3
|
||||
* https://datatables.net/download/#bs5/dt-3.1.4
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 3.1.3
|
||||
* DataTables 3.1.4
|
||||
*/
|
||||
|
||||
/*! DataTables Bootstrap 5 integration
|
||||
|
||||
Vendored
+13
-7
@@ -4,13 +4,13 @@
|
||||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs5/dt-3.1.3
|
||||
* https://datatables.net/download/#bs5/dt-3.1.4
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 3.1.3
|
||||
* DataTables 3.1.4
|
||||
*/
|
||||
|
||||
/*! DataTables 3.1.3
|
||||
/*! DataTables 3.1.4
|
||||
* Copyright (c) SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
|
||||
@@ -453,6 +453,9 @@ function assignDeep(out, ...inputs) {
|
||||
continue;
|
||||
}
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (key === '__proto__') {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (!Array.isArray(out[key])) {
|
||||
out[key] = [];
|
||||
@@ -489,6 +492,9 @@ function assignDeep(out, ...inputs) {
|
||||
function assignDeepObjects(out, extender, breakRefs = false) {
|
||||
let val;
|
||||
for (let prop in extender) {
|
||||
if (prop === '__proto__') {
|
||||
continue;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(extender, prop)) {
|
||||
val = extender[prop];
|
||||
if (plainObject(val)) {
|
||||
@@ -2694,7 +2700,7 @@ class Dom {
|
||||
*/
|
||||
prepend(content) {
|
||||
return this.each(el => {
|
||||
if (content instanceof Dom) {
|
||||
if (arrayLike(content) || dom(content)) {
|
||||
// Reverse the array, so if there are multiple elements, they
|
||||
// end up being added sequentially, just like jQuery
|
||||
let itemsReversed = Array.from(content).reverse();
|
||||
@@ -8714,7 +8720,7 @@ const ext = {
|
||||
/**
|
||||
* DataTables build type (expanded by the download builder)
|
||||
*/
|
||||
builder: 'bs5/dt-3.1.3',
|
||||
builder: 'bs5/dt-3.1.4',
|
||||
/**
|
||||
* Buttons. For use with the Buttons extension for DataTables. This is
|
||||
* defined here so other extensions can define buttons regardless of load
|
||||
@@ -8860,7 +8866,7 @@ const ext = {
|
||||
* Software version
|
||||
* @type string
|
||||
*/
|
||||
version: '3.1.3'
|
||||
version: '3.1.4'
|
||||
};
|
||||
//
|
||||
// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts
|
||||
@@ -9559,7 +9565,7 @@ register('destroy()', function (remove) {
|
||||
callbackFire(settings, 'destroy', 'destroy', [settings], true);
|
||||
// If not being removed from the document, make all columns visible
|
||||
if (!remove) {
|
||||
new Api(settings).columns().visible();
|
||||
new Api(settings).columns().visible(true);
|
||||
}
|
||||
// Container width change listener
|
||||
if (settings.resizeObserver) {
|
||||
|
||||
@@ -69,12 +69,26 @@
|
||||
</dd>
|
||||
<dt class="col-sm-5">Uses config.json</dt>
|
||||
<dd class="col-sm-7">
|
||||
{{#if page_data.overrides}}
|
||||
{{#if (eq page_data.config_file_status "Loaded")}}
|
||||
<span class="d-inline"><b>Yes</b></span>
|
||||
{{#if page_data.overrides}}
|
||||
<span class="badge bg-info text-dark abbr-badge" title="Environment variables are overwritten by a config.json.
{{page_data.overrides}}">Details</span>
|
||||
{{/if}}
|
||||
{{~else if (eq page_data.config_file_status "Error")}}
|
||||
<span class="d-inline"><b>Errors</b></span>
|
||||
<span class="badge bg-danger abbr-badge" title="Config file could not be loaded and was ignored. Check server logs for details.">Details</span>
|
||||
{{~else}}
|
||||
<span class="d-inline"><b>No</b></span>
|
||||
{{/if}}
|
||||
{{#unless page_data.overrides}}
|
||||
<span class="d-block"><b>No</b></span>
|
||||
</dd>
|
||||
<dt class="col-sm-5">Environment variables are valid</dt>
|
||||
<dd class="col-sm-7">
|
||||
{{#if page_data.invalid_env_vars}}
|
||||
<span class="d-inline"><b>No</b></span>
|
||||
<span class="badge bg-danger abbr-badge" title="Invalid environment variables are found and defaults are used instead.
{{page_data.invalid_env_vars}}">Details</span>
|
||||
{{/if}}
|
||||
{{#unless page_data.invalid_env_vars}}
|
||||
<span class="d-block"><b>Yes</b></span>
|
||||
{{/unless}}
|
||||
</dd>
|
||||
<dt class="col-sm-5">Uses custom templates</dt>
|
||||
|
||||
+20
-6
@@ -210,7 +210,7 @@ impl Fairing for Cors {
|
||||
pub struct Cached<R> {
|
||||
response: R,
|
||||
is_immutable: bool,
|
||||
ttl: u64,
|
||||
ttl: u32,
|
||||
}
|
||||
|
||||
impl<R> Cached<R> {
|
||||
@@ -230,7 +230,7 @@ impl<R> Cached<R> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ttl(response: R, ttl: u64, is_immutable: bool) -> Cached<R> {
|
||||
pub fn ttl(response: R, ttl: u32, is_immutable: bool) -> Cached<R> {
|
||||
Self {
|
||||
response,
|
||||
is_immutable,
|
||||
@@ -251,7 +251,7 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache
|
||||
res.set_raw_header("Cache-Control", cache_control_header);
|
||||
|
||||
let time_now = Local::now();
|
||||
let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.try_into().unwrap()).unwrap();
|
||||
let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.into()).unwrap();
|
||||
res.set_raw_header("Expires", format_datetime_http(&expiry_time));
|
||||
Ok(res)
|
||||
}
|
||||
@@ -447,20 +447,34 @@ pub fn get_env_str_value(key: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_env<V>(key: &str) -> Option<V>
|
||||
pub static INVALID_ENV_VARS: std::sync::Mutex<Vec<&'static str>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
pub fn get_env<V>(key: &'static str) -> Option<V>
|
||||
where
|
||||
V: FromStr,
|
||||
{
|
||||
try_parse_string(get_env_str_value(key))
|
||||
let value = get_env_str_value(key);
|
||||
let value_is_some = value.is_some();
|
||||
let parsed: Option<V> = try_parse_string(value);
|
||||
if parsed.is_none() && value_is_some {
|
||||
println!("[ERROR] `{key}` contains an invalid value for its type and is ignored, the default is used instead.");
|
||||
INVALID_ENV_VARS.lock().unwrap().push(key);
|
||||
}
|
||||
parsed
|
||||
}
|
||||
|
||||
pub fn get_env_bool(key: &str) -> Option<bool> {
|
||||
pub fn get_env_bool(key: &'static str) -> Option<bool> {
|
||||
const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"];
|
||||
const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"];
|
||||
|
||||
match get_env_str_value(key) {
|
||||
Some(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true),
|
||||
Some(val) if FALSE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(false),
|
||||
Some(_) => {
|
||||
println!("[ERROR] `{key}` contains an invalid boolean value and is ignored, the default is used instead.");
|
||||
INVALID_ENV_VARS.lock().unwrap().push(key);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user