Files
vaultwarden/src/db/models/two_factor_incomplete.rs
T
Mathijs van Veluw b71cb7a832 Misc fixes regarding jobs and config (#7858)
* Fix several scheduled job issues

Several jobs could cause a panic, or stop without running through all
items because of this panic. This could cause missed items until that
panic somehow resolved it self, and probably would never work fully
again.

This could cause emergency access for example to never work again.
Besides this, emergency access was also able to be panic if the
wait-time was out-of-bounds and date/time calculation might fail.
This is also fixed now by using `checked` functions and continue upon
those issues so all awaiting items will be handled.

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

* Fix several possible panics and overflows

Adjust several integer types to be a lower or unsigned type.
Most types were too large, only because the function which used this
value needed that specific type. But, this allowed too large values
which either caused panics or might cause a DoS.

This does however might possible cause configurations which have larger
values, which are probably way too large anyway to be not loaded or use
their defaults. During the config loading, either via env or json, it
will show error logs regarding these items.

Also fixed an issue with protected actions via OTP, it increased the
attempt before it validated if the attempt limit was reached. This could
cause an issue when set to `1`, that way it was never allowed, and `3`
times would actually be `2`.

Also added several config validation checks which otherwise would have
prevent Vaultwarden to start, or login, so these will probably not cause
issues, else users would already have issues.

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

* Show config errors on diagnostics page

With the previous commits it is now possible to add a warning if their
rare issues with loading of the config, either environment variables
which we were not able to parse correctly, or if loading the
`config.json` failed instead of just showing it wasn't used, while it
does exists, which might be confusing.

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

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2026-10-22 14:20:03 +02:00

123 lines
4.3 KiB
Rust

use chrono::{NaiveDateTime, Utc};
use diesel::prelude::*;
use crate::{
CONFIG,
api::EmptyResult,
auth::ClientIp,
db::{
DbConn,
models::{DeviceId, UserId},
schema::twofactor_incomplete,
},
error::MapResult,
};
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = twofactor_incomplete)]
#[diesel(primary_key(user_uuid, device_uuid))]
pub struct TwoFactorIncomplete {
pub user_uuid: UserId,
// This device UUID is simply what's claimed by the device. It doesn't
// necessarily correspond to any UUID in the devices table, since a device
// must complete 2FA login before being added into the devices table.
pub device_uuid: DeviceId,
pub device_name: String,
pub device_type: i32,
pub login_time: NaiveDateTime,
pub ip_address: String,
}
impl TwoFactorIncomplete {
pub async fn mark_incomplete(
user_uuid: &UserId,
device_uuid: &DeviceId,
device_name: &str,
device_type: i32,
ip: &ClientIp,
conn: &DbConn,
) -> EmptyResult {
if CONFIG.incomplete_2fa_time_limit() == 0 || !CONFIG.mail_enabled() {
return Ok(());
}
// Don't update the data for an existing user/device pair, since that
// would allow an attacker to arbitrarily delay notifications by
// sending repeated 2FA attempts to reset the timer.
let existing = Self::find_by_user_and_device(user_uuid, device_uuid, conn).await;
if existing.is_some() {
return Ok(());
}
conn.run(move |conn| {
diesel::insert_into(twofactor_incomplete::table)
.values((
twofactor_incomplete::user_uuid.eq(user_uuid),
twofactor_incomplete::device_uuid.eq(device_uuid),
twofactor_incomplete::device_name.eq(device_name),
twofactor_incomplete::device_type.eq(device_type),
twofactor_incomplete::login_time.eq(Utc::now().naive_utc()),
twofactor_incomplete::ip_address.eq(ip.ip.to_string()),
))
.execute(conn)
.map_res("Error adding twofactor_incomplete record")
})
.await
}
pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult {
if CONFIG.incomplete_2fa_time_limit() == 0 || !CONFIG.mail_enabled() {
return Ok(());
}
Self::delete_by_user_and_device(user_uuid, device_uuid, conn).await
}
pub async fn find_by_user_and_device(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> Option<Self> {
conn.run(move |conn| {
twofactor_incomplete::table
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
.filter(twofactor_incomplete::device_uuid.eq(device_uuid))
.first::<Self>(conn)
.ok()
})
.await
}
pub async fn find_logins_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {
conn.run(move |conn| {
twofactor_incomplete::table
.filter(twofactor_incomplete::login_time.lt(dt))
.load::<Self>(conn)
.expect("Error loading twofactor_incomplete")
})
.await
}
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn).await
}
pub async fn delete_by_user_and_device(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult {
conn.run(move |conn| {
diesel::delete(
twofactor_incomplete::table
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
.filter(twofactor_incomplete::device_uuid.eq(device_uuid)),
)
.execute(conn)
.map_res("Error in twofactor_incomplete::delete_by_user_and_device()")
})
.await
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
conn.run(move |conn| {
diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid)))
.execute(conn)
.map_res("Error in twofactor_incomplete::delete_all_by_user()")
})
.await
}
}