mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-08-06 19:19:09 +00:00
Merge 790822a22d
into 5d84f17600
This commit is contained in:
commit
1ef190e459
5 changed files with 112 additions and 4 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -395,6 +395,28 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-sdk-sesv2"
|
||||||
|
version = "1.85.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3037659eacd093c75005d9e8714abe39e9af9bf14e7f9f6b48f729d809fb2fc4"
|
||||||
|
dependencies = [
|
||||||
|
"aws-credential-types",
|
||||||
|
"aws-runtime",
|
||||||
|
"aws-smithy-async",
|
||||||
|
"aws-smithy-http",
|
||||||
|
"aws-smithy-json",
|
||||||
|
"aws-smithy-runtime",
|
||||||
|
"aws-smithy-runtime-api",
|
||||||
|
"aws-smithy-types",
|
||||||
|
"aws-types",
|
||||||
|
"bytes",
|
||||||
|
"fastrand",
|
||||||
|
"http 0.2.12",
|
||||||
|
"regex-lite",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-sso"
|
name = "aws-sdk-sso"
|
||||||
version = "1.78.0"
|
version = "1.78.0"
|
||||||
|
@ -592,6 +614,7 @@ dependencies = [
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"bytes",
|
"bytes",
|
||||||
"bytes-utils",
|
"bytes-utils",
|
||||||
|
"futures-core",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
|
@ -604,6 +627,8 @@ dependencies = [
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -5041,6 +5066,7 @@ dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
|
"aws-sdk-sesv2",
|
||||||
"aws-smithy-runtime-api",
|
"aws-smithy-runtime-api",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
|
@ -32,7 +32,9 @@ enable_mimalloc = ["dep:mimalloc"]
|
||||||
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
|
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
|
||||||
# if you want to turn off the logging for a specific run.
|
# if you want to turn off the logging for a specific run.
|
||||||
query_logger = ["dep:diesel_logger"]
|
query_logger = ["dep:diesel_logger"]
|
||||||
|
aws = ["s3", "ses"]
|
||||||
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:anyhow", "dep:http", "dep:reqsign"]
|
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:anyhow", "dep:http", "dep:reqsign"]
|
||||||
|
ses = ["dep:aws-config", "dep:aws-sdk-sesv2"]
|
||||||
|
|
||||||
# Enable unstable features, requires nightly
|
# Enable unstable features, requires nightly
|
||||||
# Currently only used to enable rusts official ip support
|
# Currently only used to enable rusts official ip support
|
||||||
|
@ -190,6 +192,9 @@ aws-smithy-runtime-api = { version = "1.8.5", optional = true }
|
||||||
http = { version = "1.3.1", optional = true }
|
http = { version = "1.3.1", optional = true }
|
||||||
reqsign = { version = "0.16.5", optional = true }
|
reqsign = { version = "0.16.5", optional = true }
|
||||||
|
|
||||||
|
# AWS Simple Email Service (SES) for sending emails
|
||||||
|
aws-sdk-sesv2 = { version = "1.85.0", features = ["behavior-version-latest", "rt-tokio"], default-features = false, optional = true }
|
||||||
|
|
||||||
# Strip debuginfo from the release builds
|
# Strip debuginfo from the release builds
|
||||||
# The debug symbols are to provide better panic traces
|
# The debug symbols are to provide better panic traces
|
||||||
# Also enable fat LTO and use 1 codegen unit for optimizations
|
# Also enable fat LTO and use 1 codegen unit for optimizations
|
||||||
|
|
3
build.rs
3
build.rs
|
@ -13,6 +13,8 @@ fn main() {
|
||||||
println!("cargo:rustc-cfg=query_logger");
|
println!("cargo:rustc-cfg=query_logger");
|
||||||
#[cfg(feature = "s3")]
|
#[cfg(feature = "s3")]
|
||||||
println!("cargo:rustc-cfg=s3");
|
println!("cargo:rustc-cfg=s3");
|
||||||
|
#[cfg(feature = "ses")]
|
||||||
|
println!("cargo:rustc-cfg=ses");
|
||||||
|
|
||||||
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
|
@ -26,6 +28,7 @@ fn main() {
|
||||||
println!("cargo::rustc-check-cfg=cfg(postgresql)");
|
println!("cargo::rustc-check-cfg=cfg(postgresql)");
|
||||||
println!("cargo::rustc-check-cfg=cfg(query_logger)");
|
println!("cargo::rustc-check-cfg=cfg(query_logger)");
|
||||||
println!("cargo::rustc-check-cfg=cfg(s3)");
|
println!("cargo::rustc-check-cfg=cfg(s3)");
|
||||||
|
println!("cargo::rustc-check-cfg=cfg(ses)");
|
||||||
|
|
||||||
// Rerun when these paths are changed.
|
// Rerun when these paths are changed.
|
||||||
// Someone could have checked-out a tag or specific commit, but no other files changed.
|
// Someone could have checked-out a tag or specific commit, but no other files changed.
|
||||||
|
|
|
@ -746,12 +746,14 @@ make_config! {
|
||||||
smtp_accept_invalid_certs: bool, true, def, false;
|
smtp_accept_invalid_certs: bool, true, def, false;
|
||||||
/// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
|
/// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
|
||||||
smtp_accept_invalid_hostnames: bool, true, def, false;
|
smtp_accept_invalid_hostnames: bool, true, def, false;
|
||||||
|
/// Use AWS SES |> Whether to send mail via AWS Simple Email Service (SES)
|
||||||
|
use_aws_ses: bool, true, def, false;
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Email 2FA Settings
|
/// Email 2FA Settings
|
||||||
email_2fa: _enable_email_2fa {
|
email_2fa: _enable_email_2fa {
|
||||||
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
|
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
|
||||||
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail);
|
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail || c.use_aws_ses);
|
||||||
/// 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 |> 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;
|
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.
|
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
||||||
|
@ -965,6 +967,9 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if cfg.use_aws_ses {
|
||||||
|
#[cfg(not(ses))]
|
||||||
|
err!("`USE_AWS_SES` is set, but the `ses` feature is not enabled in this build");
|
||||||
} else {
|
} else {
|
||||||
if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() {
|
if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() {
|
||||||
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`")
|
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`")
|
||||||
|
@ -975,7 +980,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !is_valid_email(&cfg.smtp_from) {
|
if (cfg.smtp_host.is_some() || cfg.use_sendmail || cfg.use_aws_ses) && !is_valid_email(&cfg.smtp_from) {
|
||||||
err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from))
|
err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -984,7 +989,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail) {
|
if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail || cfg.use_aws_ses) {
|
||||||
err!("To enable email 2FA, a mail transport must be configured")
|
err!("To enable email 2FA, a mail transport must be configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1186,6 +1191,26 @@ fn opendal_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
|
||||||
Ok(operator)
|
Ok(operator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(ses)]
|
||||||
|
pub(crate) async fn aws_sdk_config() -> &'static aws_config::SdkConfig {
|
||||||
|
use crate::http_client::aws::AwsReqwestConnector;
|
||||||
|
use aws_config::AppName;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
static AWS_CONFIG: OnceCell<aws_config::SdkConfig> = OnceCell::const_new();
|
||||||
|
|
||||||
|
AWS_CONFIG
|
||||||
|
.get_or_init(async || {
|
||||||
|
let reqwest_client = reqwest::Client::builder().build().unwrap();
|
||||||
|
let connector = AwsReqwestConnector {
|
||||||
|
client: reqwest_client,
|
||||||
|
};
|
||||||
|
|
||||||
|
aws_config::from_env().app_name(AppName::new("vaultwarden").unwrap()).http_client(connector).load().await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(s3)]
|
#[cfg(s3)]
|
||||||
fn opendal_s3_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
|
fn opendal_s3_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
|
||||||
use crate::http_client::aws::AwsReqwestConnector;
|
use crate::http_client::aws::AwsReqwestConnector;
|
||||||
|
@ -1404,7 +1429,7 @@ impl Config {
|
||||||
}
|
}
|
||||||
pub fn mail_enabled(&self) -> bool {
|
pub fn mail_enabled(&self) -> bool {
|
||||||
let inner = &self.inner.read().unwrap().config;
|
let inner = &self.inner.read().unwrap().config;
|
||||||
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
|
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail || inner.use_aws_ses)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_duo_akey(&self) -> String {
|
pub async fn get_duo_akey(&self) -> String {
|
||||||
|
|
49
src/mail.rs
49
src/mail.rs
|
@ -95,6 +95,46 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||||
smtp_client.build()
|
smtp_client.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(ses)]
|
||||||
|
async fn send_with_aws_ses(email: Message) -> std::io::Result<()> {
|
||||||
|
use std::io::Error;
|
||||||
|
|
||||||
|
use aws_sdk_sesv2::{
|
||||||
|
types::{EmailContent, RawMessage},
|
||||||
|
Client,
|
||||||
|
};
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
use crate::config::aws_sdk_config;
|
||||||
|
|
||||||
|
static AWS_SESV2_CLIENT: OnceCell<Client> = OnceCell::const_new();
|
||||||
|
|
||||||
|
let client = AWS_SESV2_CLIENT
|
||||||
|
.get_or_init(|| async {
|
||||||
|
let config = aws_sdk_config().await;
|
||||||
|
Client::new(config)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
client
|
||||||
|
.send_email()
|
||||||
|
.content(
|
||||||
|
EmailContent::builder()
|
||||||
|
.raw(
|
||||||
|
RawMessage::builder()
|
||||||
|
.data(email.formatted().into())
|
||||||
|
.build()
|
||||||
|
.map_err(|e| Error::other(format!("Failed to build AWS SESv2 RawMessage: {e:#?}")))?,
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(Error::other)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections
|
// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections
|
||||||
fn sanitize_data(data: &mut serde_json::Value) {
|
fn sanitize_data(data: &mut serde_json::Value) {
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
@ -640,6 +680,15 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if CONFIG.use_aws_ses() {
|
||||||
|
#[cfg(ses)]
|
||||||
|
match send_with_aws_ses(email).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => err!("Failed to send email", format!("Failed to send email using AWS SES: {e:?}")),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(ses))]
|
||||||
|
unreachable!("Failed to send email using AWS SES: `ses` feature is not enabled");
|
||||||
} else {
|
} else {
|
||||||
match smtp_transport().send(email).await {
|
match smtp_transport().send(email).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue