mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-08-06 22:53:44 +03:00
* Update GHA and pre-commit Signed-off-by: BlackDex <black.dex@gmail.com> * Update admin diagnostics Added a check if the templates are overridden and return which specific folder, `admin`, `email` or `scss`. This way we could more quickly point users to possible outdated templates which they are using. Also updated the Support String to use some emojis so we should be able to quicker see if there is something wrong. Just checking `true` or `false` could be difficult sometimes, and sometimes what we had as `false` wasn't bad either. Also adjusted the eslint comments so it will work with the latest version of eslint. Signed-off-by: BlackDex <black.dex@gmail.com> * Fix updating collections for a cipher The newer clients expect a `cipherDetails` response on the `collections-admin` endpoints. Without it, the client will cause an error and stops handling the update correctly. This will fix this by returning the cipher json. Fixes #7545 Fixes #7546 Signed-off-by: BlackDex <black.dex@gmail.com> * Cache CSS file in a different way Currently we set a cache ttl of 24 hours, and users need to do a force refresh if there is anything changed to the CSS file. In the past we have had several issue reported which were related to a still cached CSS file. This commit will change the caching and also cache the generated CSS file in memory. Instead of letting the browser cache it for 24 hours we generate an ETag, this is just a hash of the contents. This ETag is returned by the browser during a request, and we can match this, and if so, just return a `304` `Not Modified`. If the ETag is not known, we return the new content. This should make simple refreshes by clients get updated settings or a new version of Vaultwarden which has other CSS entries get updated instantly. If a user does a hard refresh, we will not receive the ETag and the content will be served. The same goes if someone has the `reload_templates` feature enabled, since then we should not cache anyway. If someone adjust settings via the `/admin` interface, the cache will be invalidated and a new CSS will be generated. Signed-off-by: BlackDex <black.dex@gmail.com> * Fix showing events for a specific user Signed-off-by: BlackDex <black.dex@gmail.com> * Update crates and adjust code. - Updated opendal and adjusted code where needed. - Updated yubico_ng and adjusted code where needed. This version now supports using an own HttpClient and it pulls in no reqwest dependency anymore. Now it will use our own client which uses custom hickory DNS and other features. Signed-off-by: BlackDex <black.dex@gmail.com> * Update web-vault to v2026.7.0 Signed-off-by: BlackDex <black.dex@gmail.com> * Fix hadolint warnings Signed-off-by: BlackDex <black.dex@gmail.com> --------- Signed-off-by: BlackDex <black.dex@gmail.com>
437 lines
14 KiB
Rust
437 lines
14 KiB
Rust
//
|
|
// Error generator macro
|
|
//
|
|
use std::error::Error as StdError;
|
|
|
|
use crate::db::models::EventType;
|
|
use crate::http_client::CustomHttpClientError;
|
|
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
|
|
|
macro_rules! make_error {
|
|
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {
|
|
const BAD_REQUEST: u16 = 400;
|
|
|
|
pub enum ErrorKind { $($name( $ty )),+ }
|
|
|
|
#[derive(Debug)]
|
|
pub struct ErrorEvent { pub event: EventType }
|
|
pub struct Error { message: String, kind: ErrorKind, code: u16, event: Option<ErrorEvent>, silent: bool }
|
|
|
|
$(impl From<$ty> for Error {
|
|
fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }
|
|
})+
|
|
$(impl<S: Into<String>> From<(S, $ty)> for Error {
|
|
fn from(val: (S, $ty)) -> Self {
|
|
Error { message: val.0.into(), kind: ErrorKind::$name(val.1), code: BAD_REQUEST, event: None, silent: false }
|
|
}
|
|
})+
|
|
impl StdError for Error {
|
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
|
match &self.kind {$( ErrorKind::$name(e) => $src_fn(e), )+}
|
|
}
|
|
}
|
|
impl std::fmt::Display for Error {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match &self.kind {$(
|
|
ErrorKind::$name(e) => f.write_str(&$usr_msg_fun(e, &self.message)),
|
|
)+}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
use diesel::ConnectionError as DieselConErr;
|
|
use diesel::r2d2::Error as R2d2Err;
|
|
use diesel::r2d2::PoolError as R2d2PoolErr;
|
|
use diesel::result::Error as DieselErr;
|
|
use handlebars::RenderError as HbErr;
|
|
use jsonwebtoken::errors::Error as JwtErr;
|
|
use lettre::address::AddressError as AddrErr;
|
|
use lettre::error::Error as LettreErr;
|
|
use lettre::transport::smtp::Error as SmtpErr;
|
|
use opendal::Error as OpenDALErr;
|
|
use openssl::error::ErrorStack as SSLErr;
|
|
use regex::Error as RegexErr;
|
|
use reqwest::Error as ReqErr;
|
|
use rocket::error::Error as RocketErr;
|
|
use serde_json::{Error as SerdeErr, Value};
|
|
use std::io::Error as IoErr;
|
|
use std::time::SystemTimeError as TimeErr;
|
|
use webauthn_rs::prelude::WebauthnError as WebauthnErr;
|
|
use yubico_ng::error::YubicoError as YubiErr;
|
|
|
|
#[derive(Serialize)]
|
|
pub struct Empty {}
|
|
|
|
pub struct Compact {}
|
|
|
|
// Error struct
|
|
// Contains a String error message, meant for the user and an enum variant, with an error of different types.
|
|
//
|
|
// After the variant itself, there are two expressions. The first one indicates whether the error contains a source error (that we pretty print).
|
|
// The second one contains the function used to obtain the response sent to the client
|
|
make_error! {
|
|
// Just an empty error
|
|
Empty(Empty): no_source, serialize,
|
|
// Used to represent err! calls
|
|
Simple(String): no_source, api_error,
|
|
Compact(Compact): no_source, compact_api_error,
|
|
|
|
// Used in our custom http client to handle non-global IPs and blocked domains
|
|
CustomHttpClient(CustomHttpClientError): has_source, api_error,
|
|
|
|
// Used for special return values, like 2FA errors
|
|
Json(Value): no_source, serialize,
|
|
Db(DieselErr): has_source, api_error,
|
|
R2d2(R2d2Err): has_source, api_error,
|
|
R2d2Pool(R2d2PoolErr): has_source, api_error,
|
|
Serde(SerdeErr): has_source, api_error,
|
|
JWt(JwtErr): has_source, api_error,
|
|
Handlebars(HbErr): has_source, api_error,
|
|
|
|
Io(IoErr): has_source, api_error,
|
|
Time(TimeErr): has_source, api_error,
|
|
Req(ReqErr): has_source, api_error,
|
|
Regex(RegexErr): has_source, api_error,
|
|
Yubico(YubiErr): has_source, api_error,
|
|
|
|
Lettre(LettreErr): has_source, api_error,
|
|
Address(AddrErr): has_source, api_error,
|
|
Smtp(SmtpErr): has_source, api_error,
|
|
OpenSSL(SSLErr): has_source, api_error,
|
|
Rocket(RocketErr): has_source, api_error,
|
|
|
|
DieselCon(DieselConErr): has_source, api_error,
|
|
Webauthn(WebauthnErr): has_source, api_error,
|
|
|
|
OpenDAL(OpenDALErr): has_source, api_error,
|
|
}
|
|
|
|
impl std::fmt::Debug for Error {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self.source() {
|
|
Some(e) => write!(f, "{}.\n[CAUSE] {:#?}", self.message, e),
|
|
None => match self.kind {
|
|
ErrorKind::Empty(_) => Ok(()),
|
|
ErrorKind::Simple(ref s) => {
|
|
if &self.message == s {
|
|
write!(f, "{}", self.message)
|
|
} else {
|
|
write!(f, "{}. {}", self.message, s)
|
|
}
|
|
}
|
|
ErrorKind::Json(_) => write!(f, "{}", self.message),
|
|
_ => unreachable!(),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error {
|
|
pub fn new<M: Into<String>, N: Into<String>>(usr_msg: M, log_msg: N) -> Self {
|
|
(usr_msg, log_msg.into()).into()
|
|
}
|
|
|
|
pub fn new_msg<M: Into<String> + Clone>(usr_msg: M) -> Self {
|
|
(usr_msg.clone(), usr_msg.into()).into()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn empty() -> Self {
|
|
Empty {}.into()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_msg<M: Into<String>>(mut self, msg: M) -> Self {
|
|
self.message = msg.into();
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_kind(mut self, kind: ErrorKind) -> Self {
|
|
self.kind = kind;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn with_code(mut self, code: u16) -> Self {
|
|
self.code = code;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_event(mut self, event: ErrorEvent) -> Self {
|
|
self.event = Some(event);
|
|
self
|
|
}
|
|
|
|
pub fn get_event(&self) -> &Option<ErrorEvent> {
|
|
&self.event
|
|
}
|
|
|
|
pub fn message(&self) -> &str {
|
|
&self.message
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn silent(mut self) -> Self {
|
|
self.silent = true;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_silent(mut self, silent: bool) -> Self {
|
|
self.silent = silent;
|
|
self
|
|
}
|
|
}
|
|
|
|
pub trait MapResult<S> {
|
|
fn map_res(self, msg: &str) -> Result<S, Error>;
|
|
}
|
|
|
|
impl<S, E: Into<Error>> MapResult<S> for Result<S, E> {
|
|
fn map_res(self, msg: &str) -> Result<S, Error> {
|
|
self.map_err(|e| e.into().with_msg(msg))
|
|
}
|
|
}
|
|
|
|
impl<E: Into<Error>> MapResult<()> for Result<usize, E> {
|
|
fn map_res(self, msg: &str) -> Result<(), Error> {
|
|
self.and(Ok(())).map_res(msg)
|
|
}
|
|
}
|
|
|
|
impl<S> MapResult<S> for Option<S> {
|
|
fn map_res(self, msg: &str) -> Result<S, Error> {
|
|
self.ok_or_else(|| Error::new(msg, ""))
|
|
}
|
|
}
|
|
|
|
const fn has_source<T>(e: T) -> Option<T> {
|
|
Some(e)
|
|
}
|
|
fn no_source<T, S>(_: T) -> Option<S> {
|
|
None
|
|
}
|
|
|
|
fn serialize(e: &impl Serialize, _msg: &str) -> String {
|
|
serde_json::to_string(e).unwrap()
|
|
}
|
|
|
|
/// This will serialize the default ApiErrorResponse
|
|
/// It will add the needed fields which are mostly empty or have multiple copies of the message
|
|
/// This is more efficient than having a larger struct and use the Serialize derive
|
|
/// It also prevents using `json!()` calls to create the final output
|
|
impl Serialize for ApiErrorResponse<'_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
#[derive(serde::Serialize)]
|
|
struct ErrorModel<'a> {
|
|
message: &'a str,
|
|
object: &'static str,
|
|
}
|
|
|
|
let mut state = serializer.serialize_struct("ApiErrorResponse", 9)?;
|
|
|
|
state.serialize_field("message", self.0.message)?;
|
|
|
|
let mut validation_errors = std::collections::HashMap::with_capacity(1);
|
|
validation_errors.insert("", vec![self.0.message]);
|
|
state.serialize_field("validationErrors", &validation_errors)?;
|
|
|
|
let error_model = ErrorModel {
|
|
message: self.0.message,
|
|
object: "error",
|
|
};
|
|
state.serialize_field("errorModel", &error_model)?;
|
|
|
|
state.serialize_field("error", "")?;
|
|
state.serialize_field("error_description", "")?;
|
|
state.serialize_field("exceptionMessage", &None::<()>)?;
|
|
state.serialize_field("exceptionStackTrace", &None::<()>)?;
|
|
state.serialize_field("innerExceptionMessage", &None::<()>)?;
|
|
state.serialize_field("object", "error")?;
|
|
|
|
state.end()
|
|
}
|
|
}
|
|
|
|
/// This will serialize the smaller CompactApiErrorResponse
|
|
/// It will add the needed fields which are mostly empty
|
|
/// This is more efficient than having a larger struct and use the Serialize derive
|
|
/// It also prevents using `json!()` calls to create the final output
|
|
impl Serialize for CompactApiErrorResponse<'_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let mut state = serializer.serialize_struct("CompactApiErrorResponse", 6)?;
|
|
|
|
state.serialize_field("message", self.0.message)?;
|
|
state.serialize_field("validationErrors", &None::<()>)?;
|
|
state.serialize_field("exceptionMessage", &None::<()>)?;
|
|
state.serialize_field("exceptionStackTrace", &None::<()>)?;
|
|
state.serialize_field("innerExceptionMessage", &None::<()>)?;
|
|
state.serialize_field("object", "error")?;
|
|
|
|
state.end()
|
|
}
|
|
}
|
|
|
|
/// Main API Error struct template
|
|
/// This struct which we can be used by both ApiErrorResponse and CompactApiErrorResponse
|
|
/// is small and doesn't contain unneeded empty fields. This is more memory efficient, but also less code to compile
|
|
struct ApiErrorMsg<'a> {
|
|
message: &'a str,
|
|
}
|
|
/// Default API Error response struct
|
|
/// The custom serialization adds all other needed fields
|
|
struct ApiErrorResponse<'a>(ApiErrorMsg<'a>);
|
|
/// Compact API Error response struct used for some newer error responses
|
|
/// The custom serialization adds all other needed fields
|
|
struct CompactApiErrorResponse<'a>(ApiErrorMsg<'a>);
|
|
|
|
fn api_error(_: &impl std::any::Any, msg: &str) -> String {
|
|
let response = ApiErrorMsg {
|
|
message: msg,
|
|
};
|
|
serde_json::to_string(&ApiErrorResponse(response)).unwrap()
|
|
}
|
|
|
|
fn compact_api_error(_: &impl std::any::Any, msg: &str) -> String {
|
|
let response = ApiErrorMsg {
|
|
message: msg,
|
|
};
|
|
serde_json::to_string(&CompactApiErrorResponse(response)).unwrap()
|
|
}
|
|
|
|
//
|
|
// Rocket responder impl
|
|
//
|
|
use std::io::Cursor;
|
|
|
|
use rocket::{
|
|
http::{ContentType, Status},
|
|
request::Request,
|
|
response::{self, Responder, Response},
|
|
};
|
|
|
|
impl Responder<'_, 'static> for Error {
|
|
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
|
|
if !self.silent {
|
|
match self.kind {
|
|
ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Compact(_) => {} // Don't print the error in this situation
|
|
_ => error!(target: "error", "{self:#?}"),
|
|
}
|
|
}
|
|
|
|
let code = Status::from_code(self.code).unwrap_or(Status::BadRequest);
|
|
let body = self.to_string();
|
|
Response::build().status(code).header(ContentType::JSON).sized_body(Some(body.len()), Cursor::new(body)).ok()
|
|
}
|
|
}
|
|
|
|
//
|
|
// Error return macros
|
|
//
|
|
#[macro_export]
|
|
macro_rules! err {
|
|
($kind:ident, $msg:expr) => {{
|
|
let msg = $msg;
|
|
error!("{msg}");
|
|
return Err($crate::error::Error::new_msg(msg).with_kind($crate::error::ErrorKind::$kind($crate::error::$kind {})));
|
|
}};
|
|
($msg:expr) => {{
|
|
let msg = $msg;
|
|
error!("{msg}");
|
|
return Err($crate::error::Error::new_msg(msg));
|
|
}};
|
|
($msg:expr, ErrorEvent $err_event:tt) => {{
|
|
let msg = $msg;
|
|
error!("{msg}");
|
|
return Err($crate::error::Error::new_msg(msg).with_event($crate::error::ErrorEvent $err_event));
|
|
}};
|
|
($usr_msg:expr, $log_value:expr) => {{
|
|
let usr_msg = $usr_msg;
|
|
let log_value = $log_value;
|
|
error!("{usr_msg}. {log_value}");
|
|
return Err($crate::error::Error::new(usr_msg, log_value));
|
|
}};
|
|
($usr_msg:expr, $log_value:expr, ErrorEvent $err_event:tt) => {{
|
|
let usr_msg = $usr_msg;
|
|
let log_value = $log_value;
|
|
error!("{usr_msg}. {log_value}");
|
|
return Err($crate::error::Error::new(usr_msg, log_value).with_event($crate::error::ErrorEvent $err_event));
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! err_silent {
|
|
($msg:expr) => {{
|
|
return Err($crate::error::Error::new_msg($msg));
|
|
}};
|
|
($msg:expr, ErrorEvent $err_event:tt) => {{
|
|
return Err($crate::error::Error::new_msg($msg).with_event($crate::error::ErrorEvent $err_event));
|
|
}};
|
|
($usr_msg:expr, $log_value:expr) => {{
|
|
return Err($crate::error::Error::new($usr_msg, $log_value));
|
|
}};
|
|
($usr_msg:expr, $log_value:expr, ErrorEvent $err_event:tt) => {{
|
|
return Err($crate::error::Error::new($usr_msg, $log_value).with_event($crate::error::ErrorEvent $err_event));
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! err_code {
|
|
($msg:expr, $err_code:expr) => {{
|
|
let msg = $msg;
|
|
error!("{msg}");
|
|
return Err($crate::error::Error::new_msg(msg).with_code($err_code));
|
|
}};
|
|
($usr_msg:expr, $log_value:expr, $err_code:expr) => {{
|
|
let usr_msg = $usr_msg;
|
|
let log_value = $log_value;
|
|
error!("{usr_msg}. {log_value}");
|
|
return Err($crate::error::Error::new(usr_msg, log_value).with_code($err_code));
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! err_discard {
|
|
($msg:expr, $data:expr) => {{
|
|
std::io::copy(&mut $data.open(), &mut std::io::sink()).ok();
|
|
return Err($crate::error::Error::new_msg($msg));
|
|
}};
|
|
($usr_msg:expr, $log_value:expr, $data:expr) => {{
|
|
std::io::copy(&mut $data.open(), &mut std::io::sink()).ok();
|
|
return Err($crate::error::Error::new($usr_msg, $log_value));
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! err_json {
|
|
($expr:expr, $log_value:expr) => {{
|
|
return Err(($log_value, $expr).into());
|
|
}};
|
|
($expr:expr, $log_value:expr, $err_event:expr, ErrorEvent) => {{
|
|
return Err(($log_value, $expr).into().with_event($err_event));
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! err_handler {
|
|
($expr:expr) => {{
|
|
error!(target: "auth", "Unauthorized Error: {}", $expr);
|
|
return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, $expr));
|
|
}};
|
|
($usr_msg:expr, $log_value:expr) => {{
|
|
let usr_msg = $usr_msg;
|
|
let log_value = $log_value;
|
|
error!(target: "auth", "Unauthorized Error: {usr_msg}. {log_value}");
|
|
return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, usr_msg));
|
|
}};
|
|
}
|