Browse Source

Merge 3f57c7c322 into 0cefa4cca7

pull/5910/merge
Chase Douglas 6 days ago
committed by GitHub
parent
commit
bcab73f356
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 128
      Cargo.lock
  2. 3
      Cargo.toml
  3. 6
      build.rs
  4. 26
      src/aws.rs
  5. 13
      src/config.rs
  6. 2
      src/http_client.rs
  7. 47
      src/mail.rs
  8. 2
      src/main.rs
  9. 24
      src/storage.rs

128
Cargo.lock

@ -359,11 +359,11 @@ dependencies = [
"aws-sdk-ssooidc",
"aws-sdk-sts",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-http 0.64.0",
"aws-smithy-json 0.63.0",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-schema",
"aws-smithy-schema 0.2.0",
"aws-smithy-types",
"aws-types",
"bytes",
@ -399,7 +399,7 @@ dependencies = [
"aws-credential-types",
"aws-sigv4",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-http 0.64.0",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
@ -415,6 +415,30 @@ dependencies = [
"uuid",
]
[[package]]
name = "aws-sdk-sesv2"
version = "1.118.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8d0642857f4fe76cd9a3d8c4f2b393546f7561f7725052dd9f268005fda92b7"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http 0.63.6",
"aws-smithy-json 0.62.7",
"aws-smithy-observability 0.2.6",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"http 0.2.12",
"http 1.4.2",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sdk-sso"
version = "1.105.0"
@ -425,12 +449,12 @@ dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-observability",
"aws-smithy-http 0.64.0",
"aws-smithy-json 0.63.0",
"aws-smithy-observability 0.3.0",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-schema",
"aws-smithy-schema 0.2.0",
"aws-smithy-types",
"aws-types",
"bytes",
@ -451,12 +475,12 @@ dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-observability",
"aws-smithy-http 0.64.0",
"aws-smithy-json 0.63.0",
"aws-smithy-observability 0.3.0",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-schema",
"aws-smithy-schema 0.2.0",
"aws-smithy-types",
"aws-types",
"bytes",
@ -477,13 +501,13 @@ dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-observability",
"aws-smithy-http 0.64.0",
"aws-smithy-json 0.63.0",
"aws-smithy-observability 0.3.0",
"aws-smithy-query",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-schema",
"aws-smithy-schema 0.2.0",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
@ -501,7 +525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f"
dependencies = [
"aws-credential-types",
"aws-smithy-http",
"aws-smithy-http 0.64.0",
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
@ -527,6 +551,27 @@ dependencies = [
"tokio",
]
[[package]]
name = "aws-smithy-http"
version = "0.63.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"bytes-utils",
"futures-core",
"futures-util",
"http 1.4.2",
"http-body 1.1.0",
"http-body-util",
"percent-encoding",
"pin-project-lite",
"pin-utils",
"tracing",
]
[[package]]
name = "aws-smithy-http"
version = "0.64.0"
@ -548,6 +593,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "aws-smithy-json"
version = "0.62.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-schema 0.1.0",
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-json"
version = "0.63.0"
@ -555,10 +611,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-schema",
"aws-smithy-schema 0.2.0",
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-observability"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c"
dependencies = [
"aws-smithy-runtime-api",
]
[[package]]
name = "aws-smithy-observability"
version = "0.3.0"
@ -575,7 +640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-schema",
"aws-smithy-schema 0.2.0",
"aws-smithy-types",
"aws-smithy-xml",
"urlencoding",
@ -588,10 +653,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-observability",
"aws-smithy-http 0.64.0",
"aws-smithy-observability 0.3.0",
"aws-smithy-runtime-api",
"aws-smithy-schema",
"aws-smithy-schema 0.2.0",
"aws-smithy-types",
"bytes",
"fastrand",
@ -635,6 +700,17 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "aws-smithy-schema"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"http 1.4.2",
]
[[package]]
name = "aws-smithy-schema"
version = "0.2.0"
@ -655,6 +731,7 @@ dependencies = [
"base64-simd",
"bytes",
"bytes-utils",
"futures-core",
"http 0.2.12",
"http 1.5.0",
"http-body 0.4.6",
@ -667,6 +744,8 @@ dependencies = [
"ryu",
"serde",
"time",
"tokio",
"tokio-util",
]
[[package]]
@ -676,7 +755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-schema",
"aws-smithy-schema 0.2.0",
"aws-smithy-types",
"xmlparser",
]
@ -690,7 +769,7 @@ dependencies = [
"aws-credential-types",
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-schema",
"aws-smithy-schema 0.2.0",
"aws-smithy-types",
"rustc_version",
"tracing",
@ -5833,6 +5912,7 @@ dependencies = [
"argon2",
"aws-config",
"aws-credential-types",
"aws-sdk-sesv2",
"aws-smithy-runtime-api",
"bigdecimal",
"bytes",

3
Cargo.toml

@ -39,6 +39,7 @@ vendored_openssl = ["openssl/vendored"]
# Enable MiMalloc memory allocator to replace the default malloc
# This can improve performance for Alpine builds
enable_mimalloc = ["dep:mimalloc"]
aws = ["s3", "ses"]
s3 = [
"opendal/services-s3",
"dep:aws-config",
@ -48,6 +49,7 @@ s3 = [
"dep:reqsign-aws-v4",
"dep:reqsign-core",
]
ses = ["dep:aws-config", "dep:aws-sdk-sesv2", "dep:aws-smithy-runtime-api"]
# OIDC specific features
oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"]
@ -266,6 +268,7 @@ aws-config = { version = "1.10.1", optional = true, default-features = false, fe
"sso",
] }
aws-credential-types = { version = "1.3.0", optional = true }
aws-sdk-sesv2 = { version = "1.118.0", features = ["behavior-version-latest", "rt-tokio"], default-features = false, optional = true }
aws-smithy-runtime-api = { version = "1.14.0", optional = true }
http = { version = "1.5.0", optional = true }
reqsign-aws-v4 = { version = "3.1.0", optional = true }

6
build.rs

@ -15,6 +15,10 @@ fn main() {
#[cfg(feature = "s3")]
println!("cargo:rustc-cfg=s3");
#[cfg(feature = "ses")]
println!("cargo:rustc-cfg=ses");
#[cfg(feature = "aws")]
println!("cargo:rustc-cfg=aws");
// Use check-cfg to let cargo know which cfg's we define,
// and avoid warnings when they are used in the code.
@ -22,6 +26,8 @@ fn main() {
println!("cargo::rustc-check-cfg=cfg(mysql)");
println!("cargo::rustc-check-cfg=cfg(postgresql)");
println!("cargo::rustc-check-cfg=cfg(s3)");
println!("cargo::rustc-check-cfg=cfg(ses)");
println!("cargo::rustc-check-cfg=cfg(aws)");
// Rerun when these paths are changed.
// Someone could have checked-out a tag or specific commit, but no other files changed.

26
src/aws.rs

@ -0,0 +1,26 @@
use aws_config::{AppName, BehaviorVersion};
use tokio::sync::OnceCell;
use crate::http_client::aws::AwsReqwestConnector;
fn aws_reqwest_connector() -> AwsReqwestConnector {
let reqwest_client = reqwest::Client::builder().build().expect("Failed to build reqwest client");
AwsReqwestConnector {
client: reqwest_client,
}
}
pub(crate) async fn aws_sdk_config() -> &'static aws_config::SdkConfig {
static AWS_CONFIG: OnceCell<aws_config::SdkConfig> = OnceCell::const_new();
AWS_CONFIG
.get_or_init(async || {
aws_config::defaults(BehaviorVersion::latest())
.app_name(AppName::new("vaultwarden").expect("Failed to build AWS app name"))
.http_client(aws_reqwest_connector())
.load()
.await
})
.await
}

13
src/config.rs

@ -917,12 +917,14 @@ make_config! {
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!
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: _enable_email_2fa {
/// 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: 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.
@ -1170,6 +1172,9 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> 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 {
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`")
@ -1182,7 +1187,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> 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))
}
@ -1191,7 +1196,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> 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")
}
@ -1602,7 +1607,7 @@ impl Config {
}
pub fn mail_enabled(&self) -> bool {
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 {

2
src/http_client.rs

@ -329,7 +329,7 @@ impl Resolve for CustomDns {
}
}
#[cfg(s3)]
#[cfg(any(s3, ses))]
pub(crate) mod aws {
use aws_smithy_runtime_api::client::{
http::{HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector},

47
src/mail.rs

@ -96,6 +96,44 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
smtp_client.build()
}
#[cfg(ses)]
async fn send_with_aws_ses(email: Message) -> std::io::Result<()> {
use std::io::Error;
use aws_sdk_sesv2::{
Client,
types::{EmailContent, RawMessage},
};
use tokio::sync::OnceCell;
static AWS_SESV2_CLIENT: OnceCell<Client> = OnceCell::const_new();
let client = AWS_SESV2_CLIENT
.get_or_init(async || {
let config = crate::aws::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
fn sanitize_data(data: &mut serde_json::Value) {
use regex::Regex;
@ -676,6 +714,15 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
err!(format!("Sendmail error: {e}"));
}
}
} 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 {
match smtp_transport().send(email).await {
Ok(_) => Ok(()),

2
src/main.rs

@ -51,6 +51,8 @@ use rocket::data::{Limits, ToByteUnit};
mod error;
mod api;
mod auth;
#[cfg(any(s3, ses))]
mod aws;
mod config;
mod crypto;
#[macro_use]

24
src/storage.rs

@ -152,8 +152,6 @@ mod s3 {
}
pub(super) fn operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
use crate::http_client::aws::AwsReqwestConnector;
use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig};
use opendal::Configurator;
use reqsign_aws_v4::Credential;
use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain};
@ -171,24 +169,12 @@ mod s3 {
async fn provide_credential(&self, _ctx: &Context) -> reqsign_core::Result<Option<Self::Credential>> {
use aws_credential_types::provider::ProvideCredentials as _;
use reqsign_core::time::Timestamp;
use tokio::sync::OnceCell;
static DEFAULT_CREDENTIAL_CHAIN: OnceCell<DefaultCredentialsChain> = OnceCell::const_new();
let chain = DEFAULT_CREDENTIAL_CHAIN
.get_or_init(|| {
let reqwest_client = reqwest::Client::builder().build().unwrap();
let connector = AwsReqwestConnector {
client: reqwest_client,
};
let conf = ProviderConfig::default().with_http_client(connector);
DefaultCredentialsChain::builder().configure(conf).build()
})
.await;
let creds = chain.provide_credentials().await.map_err(|e| {
let credentials_provider =
crate::aws::aws_sdk_config().await.credentials_provider().ok_or_else(|| {
reqsign_core::Error::unexpected("failed to load AWS credentials provider from AWS SDK config")
})?;
let creds = credentials_provider.provide_credentials().await.map_err(|e| {
reqsign_core::Error::unexpected("failed to load AWS credentials via AWS SDK").with_source(e)
})?;

Loading…
Cancel
Save