mirror of
https://github.com/ViViDboarder/bitwarden_rs.git
synced 2024-12-22 11:37:45 +00:00
Config can now be serialized / deserialized
This commit is contained in:
parent
20d8d800f3
commit
86ed75bf7c
@ -24,6 +24,8 @@ pub fn routes() -> Vec<Route> {
|
||||
invite_user,
|
||||
delete_user,
|
||||
deauth_user,
|
||||
get_config,
|
||||
post_config,
|
||||
]
|
||||
}
|
||||
|
||||
@ -136,11 +138,11 @@ fn invite_user(data: JsonUpcase<InviteData>, _token: AdminToken, conn: DbConn) -
|
||||
err!("Invitations are not allowed")
|
||||
}
|
||||
|
||||
if let Some(ref mail_config) = CONFIG.mail() {
|
||||
if CONFIG.mail_enabled() {
|
||||
let mut user = User::new(email);
|
||||
user.save(&conn)?;
|
||||
let org_name = "bitwarden_rs";
|
||||
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None, mail_config)
|
||||
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
|
||||
} else {
|
||||
let mut invitation = Invitation::new(data.Email);
|
||||
invitation.save(&conn)
|
||||
@ -169,6 +171,20 @@ fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||
user.save(&conn)
|
||||
}
|
||||
|
||||
#[get("/config")]
|
||||
fn get_config(_token: AdminToken) -> EmptyResult {
|
||||
unimplemented!("Get config")
|
||||
}
|
||||
|
||||
#[post("/config", data = "<data>")]
|
||||
fn post_config(data: JsonUpcase<Value>, _token: AdminToken) -> EmptyResult {
|
||||
let data: Value = data.into_inner().data;
|
||||
|
||||
info!("CONFIG: {:#?}", data);
|
||||
|
||||
unimplemented!("Update config")
|
||||
}
|
||||
|
||||
pub struct AdminToken {}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
|
||||
|
@ -419,8 +419,8 @@ fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResul
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if let Some(ref mail_config) = CONFIG.mail() {
|
||||
mail::send_password_hint(&data.Email, hint, mail_config)?;
|
||||
if CONFIG.mail_enabled() {
|
||||
mail::send_password_hint(&data.Email, hint)?;
|
||||
} else if CONFIG.show_password_hint() {
|
||||
if let Some(hint) = hint {
|
||||
err!(format!("Your password hint is: {}", &hint));
|
||||
|
@ -486,9 +486,9 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
}
|
||||
|
||||
for email in data.Emails.iter() {
|
||||
let mut user_org_status = match CONFIG.mail() {
|
||||
Some(_) => UserOrgStatus::Invited as i32,
|
||||
None => UserOrgStatus::Accepted as i32, // Automatically mark user as accepted if no email invites
|
||||
let mut user_org_status = match CONFIG.mail_enabled() {
|
||||
true => UserOrgStatus::Invited as i32,
|
||||
false => UserOrgStatus::Accepted as i32, // Automatically mark user as accepted if no email invites
|
||||
};
|
||||
let user = match User::find_by_mail(&email, &conn) {
|
||||
None => {
|
||||
@ -496,7 +496,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
err!(format!("User email does not exist: {}", email))
|
||||
}
|
||||
|
||||
if CONFIG.mail().is_none() {
|
||||
if !CONFIG.mail_enabled() {
|
||||
let mut invitation = Invitation::new(email.clone());
|
||||
invitation.save(&conn)?;
|
||||
}
|
||||
@ -535,7 +535,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
|
||||
new_user.save(&conn)?;
|
||||
|
||||
if let Some(ref mail_config) = CONFIG.mail() {
|
||||
if CONFIG.mail_enabled() {
|
||||
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
||||
Some(org) => org.name,
|
||||
None => err!("Error looking up organization"),
|
||||
@ -548,7 +548,6 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
Some(new_user.uuid),
|
||||
&org_name,
|
||||
Some(headers.user.email.clone()),
|
||||
mail_config,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@ -562,7 +561,7 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
|
||||
err!("Invitations are not allowed.")
|
||||
}
|
||||
|
||||
if CONFIG.mail().is_none() {
|
||||
if !CONFIG.mail_enabled() {
|
||||
err!("SMTP is not configured.")
|
||||
}
|
||||
|
||||
@ -585,7 +584,7 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
|
||||
None => err!("Error looking up organization."),
|
||||
};
|
||||
|
||||
if let Some(ref mail_config) = CONFIG.mail() {
|
||||
if CONFIG.mail_enabled() {
|
||||
mail::send_invite(
|
||||
&user.email,
|
||||
&user.uuid,
|
||||
@ -593,7 +592,6 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
|
||||
Some(user_org.uuid),
|
||||
&org_name,
|
||||
Some(headers.user.email),
|
||||
mail_config,
|
||||
)?;
|
||||
} else {
|
||||
let mut invitation = Invitation::new(user.email.clone());
|
||||
@ -637,7 +635,7 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
|
||||
None => err!("Invited user not found"),
|
||||
}
|
||||
|
||||
if let Some(ref mail_config) = CONFIG.mail() {
|
||||
if CONFIG.mail_enabled() {
|
||||
let mut org_name = String::from("bitwarden_rs");
|
||||
if let Some(org_id) = &claims.org_id {
|
||||
org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
||||
@ -647,10 +645,10 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
|
||||
};
|
||||
if let Some(invited_by_email) = &claims.invited_by_email {
|
||||
// User was invited to an organization, so they must be confirmed manually after acceptance
|
||||
mail::send_invite_accepted(&claims.email, invited_by_email, &org_name, mail_config)?;
|
||||
mail::send_invite_accepted(&claims.email, invited_by_email, &org_name)?;
|
||||
} else {
|
||||
// User was invited from /admin, so they are automatically confirmed
|
||||
mail::send_invite_confirmed(&claims.email, &org_name, mail_config)?;
|
||||
mail::send_invite_confirmed(&claims.email, &org_name)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -686,7 +684,7 @@ fn confirm_invite(
|
||||
None => err!("Invalid key provided"),
|
||||
};
|
||||
|
||||
if let Some(ref mail_config) = CONFIG.mail() {
|
||||
if CONFIG.mail_enabled() {
|
||||
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
||||
Some(org) => org.name,
|
||||
None => err!("Error looking up organization."),
|
||||
@ -695,7 +693,7 @@ fn confirm_invite(
|
||||
Some(user) => user.email,
|
||||
None => err!("Error looking up user."),
|
||||
};
|
||||
mail::send_invite_confirmed(&address, &org_name, mail_config)?;
|
||||
mail::send_invite_confirmed(&address, &org_name)?;
|
||||
}
|
||||
|
||||
user_to_confirm.save(&conn)
|
||||
|
@ -3,15 +3,14 @@ use rocket_contrib::json::Json;
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||
use crate::auth::Headers;
|
||||
use crate::crypto;
|
||||
use crate::db::{
|
||||
models::{TwoFactor, TwoFactorType, User},
|
||||
DbConn,
|
||||
};
|
||||
|
||||
use crate::crypto;
|
||||
|
||||
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||
use crate::auth::Headers;
|
||||
use crate::error::{Error, MapResult};
|
||||
|
||||
use rocket::Route;
|
||||
|
||||
@ -508,32 +507,31 @@ fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
|
||||
result
|
||||
}
|
||||
|
||||
fn verify_yubikey_otp(otp: String) -> JsonResult {
|
||||
if !CONFIG.yubico_cred_set() {
|
||||
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled")
|
||||
fn get_yubico_credentials() -> Result<(String, String), Error> {
|
||||
match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
|
||||
(Some(id), Some(secret)) => Ok((id, secret)),
|
||||
_ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_yubikey_otp(otp: String) -> EmptyResult {
|
||||
let (yubico_id, yubico_secret) = get_yubico_credentials()?;
|
||||
|
||||
let yubico = Yubico::new();
|
||||
let config = Config::default()
|
||||
.set_client_id(CONFIG.yubico_client_id())
|
||||
.set_key(CONFIG.yubico_secret_key());
|
||||
let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
|
||||
|
||||
let result = match CONFIG.yubico_server() {
|
||||
match CONFIG.yubico_server() {
|
||||
Some(server) => yubico.verify(otp, config.set_api_hosts(vec![server])),
|
||||
None => yubico.verify(otp, config),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_answer) => Ok(Json(json!({}))),
|
||||
Err(_e) => err!("Failed to verify OTP"),
|
||||
}
|
||||
.map_res("Failed to verify OTP")
|
||||
.and(Ok(()))
|
||||
}
|
||||
|
||||
#[post("/two-factor/get-yubikey", data = "<data>")]
|
||||
fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
if !CONFIG.yubico_cred_set() {
|
||||
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled")
|
||||
}
|
||||
// Make sure the credentials are set
|
||||
get_yubico_credentials()?;
|
||||
|
||||
let data: PasswordData = data.into_inner().data;
|
||||
let user = headers.user;
|
||||
@ -597,11 +595,7 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = verify_yubikey_otp(yubikey.to_owned());
|
||||
|
||||
if let Err(_e) = result {
|
||||
err!("Invalid Yubikey OTP provided");
|
||||
}
|
||||
verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
|
||||
}
|
||||
|
||||
let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
|
||||
|
@ -355,7 +355,7 @@ pub fn start_notification_server() -> WebSocketUsers {
|
||||
thread::spawn(move || {
|
||||
WebSocket::new(factory)
|
||||
.unwrap()
|
||||
.listen(&CONFIG.websocket_url())
|
||||
.listen((CONFIG.websocket_address().as_str(), CONFIG.websocket_port()))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
297
src/config.rs
297
src/config.rs
@ -3,77 +3,149 @@ use std::sync::RwLock;
|
||||
|
||||
use handlebars::Handlebars;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::util::IntoResult;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: Config = Config::load();
|
||||
pub static ref CONFIG: Config = Config::load().unwrap_or_else(|e| {
|
||||
println!("Error loading config:\n\t{:?}\n", e);
|
||||
exit(12)
|
||||
});
|
||||
}
|
||||
|
||||
macro_rules! make_config {
|
||||
( $( $name:ident: $ty:ty ),+ $(,)* ) => {
|
||||
( $( $name:ident : $ty:ty $(, $default_fn:expr)? );+ $(;)* ) => {
|
||||
|
||||
pub struct Config { inner: RwLock<_Config> }
|
||||
pub struct Config { inner: RwLock<Inner> }
|
||||
|
||||
#[derive(Default)]
|
||||
struct _Config {
|
||||
_templates: Handlebars,
|
||||
$(pub $name: $ty),+
|
||||
struct Inner {
|
||||
templates: Handlebars,
|
||||
config: _Config,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
struct _Config { $(pub $name: $ty),+ }
|
||||
|
||||
paste::item! {
|
||||
#[allow(unused)]
|
||||
impl Config {
|
||||
#[allow(unused)]
|
||||
impl Config {
|
||||
$(
|
||||
pub fn $name(&self) -> $ty {
|
||||
self.inner.read().unwrap().config.$name.clone()
|
||||
}
|
||||
pub fn [<set_ $name>](&self, value: $ty) {
|
||||
self.inner.write().unwrap().config.$name = value;
|
||||
}
|
||||
)+
|
||||
|
||||
pub fn load() -> Result<Self, Error> {
|
||||
use crate::util::get_env;
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let mut config = _Config::default();
|
||||
|
||||
$(
|
||||
pub fn $name(&self) -> $ty {
|
||||
self.inner.read().unwrap().$name.clone()
|
||||
}
|
||||
pub fn [<set_ $name>](&self, value: $ty) {
|
||||
self.inner.write().unwrap().$name = value;
|
||||
}
|
||||
config.$name = make_config!{ @expr &stringify!($name).to_uppercase(), $ty, &config, $($default_fn)? };
|
||||
)+
|
||||
|
||||
Ok(Config {
|
||||
inner: RwLock::new(Inner {
|
||||
templates: load_templates(&config.templates_folder),
|
||||
config,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
( @expr $name:expr, $ty:ty, $config:expr, $default_fn:expr ) => {{
|
||||
match get_env($name) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
let f: &Fn(&_Config) -> _ = &$default_fn;
|
||||
f($config).into_result()?
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
( @expr $name:expr, $ty:ty, $config:expr, ) => {
|
||||
get_env($name)
|
||||
};
|
||||
}
|
||||
|
||||
make_config! {
|
||||
database_url: String,
|
||||
icon_cache_folder: String,
|
||||
attachments_folder: String,
|
||||
data_folder: String, |_| "data".to_string();
|
||||
database_url: String, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
|
||||
icon_cache_folder: String, |c| format!("{}/{}", c.data_folder, "icon_cache");
|
||||
attachments_folder: String, |c| format!("{}/{}", c.data_folder, "attachments");
|
||||
templates_folder: String, |c| format!("{}/{}", c.data_folder, "templates");
|
||||
|
||||
icon_cache_ttl: u64,
|
||||
icon_cache_negttl: u64,
|
||||
rsa_key_filename: String, |c| format!("{}/{}", c.data_folder, "rsa_key");
|
||||
private_rsa_key: String, |c| format!("{}.der", c.rsa_key_filename);
|
||||
private_rsa_key_pem: String, |c| format!("{}.pem", c.rsa_key_filename);
|
||||
public_rsa_key: String, |c| format!("{}.pub.der", c.rsa_key_filename);
|
||||
|
||||
private_rsa_key: String,
|
||||
private_rsa_key_pem: String,
|
||||
public_rsa_key: String,
|
||||
websocket_enabled: bool, |_| false;
|
||||
websocket_address: String, |_| "0.0.0.0".to_string();
|
||||
websocket_port: u16, |_| 3012;
|
||||
|
||||
web_vault_folder: String,
|
||||
web_vault_enabled: bool,
|
||||
web_vault_folder: String, |_| "web-vault/".to_string();
|
||||
web_vault_enabled: bool, |_| true;
|
||||
|
||||
websocket_enabled: bool,
|
||||
websocket_url: String,
|
||||
icon_cache_ttl: u64, |_| 2_592_000;
|
||||
icon_cache_negttl: u64, |_| 259_200;
|
||||
|
||||
extended_logging: bool,
|
||||
log_file: Option<String>,
|
||||
disable_icon_download: bool, |_| false;
|
||||
signups_allowed: bool, |_| true;
|
||||
invitations_allowed: bool, |_| true;
|
||||
password_iterations: i32, |_| 100_000;
|
||||
show_password_hint: bool, |_| true;
|
||||
|
||||
disable_icon_download: bool,
|
||||
signups_allowed: bool,
|
||||
invitations_allowed: bool,
|
||||
admin_token: Option<String>,
|
||||
password_iterations: i32,
|
||||
show_password_hint: bool,
|
||||
domain: String, |_| "http://localhost".to_string();
|
||||
domain_set: bool, |_| false;
|
||||
|
||||
domain: String,
|
||||
domain_set: bool,
|
||||
reload_templates: bool, |_| false;
|
||||
|
||||
yubico_cred_set: bool,
|
||||
yubico_client_id: String,
|
||||
yubico_secret_key: String,
|
||||
yubico_server: Option<String>,
|
||||
extended_logging: bool, |_| true;
|
||||
log_file: Option<String>;
|
||||
|
||||
mail: Option<MailConfig>,
|
||||
templates_folder: String,
|
||||
reload_templates: bool,
|
||||
admin_token: Option<String>;
|
||||
|
||||
yubico_client_id: Option<String>;
|
||||
yubico_secret_key: Option<String>;
|
||||
yubico_server: Option<String>;
|
||||
|
||||
// Mail settings
|
||||
smtp_host: Option<String>;
|
||||
smtp_ssl: bool, |_| true;
|
||||
smtp_port: u16, |c| if c.smtp_ssl {587} else {25};
|
||||
smtp_from: String, |c| if c.smtp_host.is_some() { err!("Please specify SMTP_FROM to enable SMTP support") } else { Ok(String::new() )};
|
||||
smtp_from_name: String, |_| "Bitwarden_RS".to_string();
|
||||
smtp_username: Option<String>;
|
||||
smtp_password: Option<String>;
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn mail_enabled(&self) -> bool {
|
||||
self.inner.read().unwrap().config.smtp_host.is_some()
|
||||
}
|
||||
|
||||
pub fn render_template<T: serde::ser::Serialize>(
|
||||
&self,
|
||||
name: &str,
|
||||
data: &T,
|
||||
) -> Result<String, crate::error::Error> {
|
||||
if CONFIG.reload_templates() {
|
||||
warn!("RELOADING TEMPLATES");
|
||||
let hb = load_templates(CONFIG.templates_folder().as_ref());
|
||||
hb.render(name, data).map_err(Into::into)
|
||||
} else {
|
||||
let hb = &CONFIG.inner.read().unwrap().templates;
|
||||
hb.render(name, data).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_templates(path: &str) -> Handlebars {
|
||||
@ -106,140 +178,3 @@ fn load_templates(path: &str) -> Handlebars {
|
||||
|
||||
hb
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn render_template<T: serde::ser::Serialize>(
|
||||
&self,
|
||||
name: &str,
|
||||
data: &T,
|
||||
) -> Result<String, crate::error::Error> {
|
||||
if CONFIG.reload_templates() {
|
||||
warn!("RELOADING TEMPLATES");
|
||||
let hb = load_templates(CONFIG.templates_folder().as_ref());
|
||||
hb.render(name, data).map_err(Into::into)
|
||||
} else {
|
||||
let hb = &CONFIG.inner.read().unwrap()._templates;
|
||||
hb.render(name, data).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
fn load() -> Self {
|
||||
use crate::util::{get_env, get_env_or};
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let df = get_env_or("DATA_FOLDER", "data".to_string());
|
||||
let key = get_env_or("RSA_KEY_FILENAME", format!("{}/{}", &df, "rsa_key"));
|
||||
|
||||
let domain = get_env("DOMAIN");
|
||||
|
||||
let yubico_client_id = get_env("YUBICO_CLIENT_ID");
|
||||
let yubico_secret_key = get_env("YUBICO_SECRET_KEY");
|
||||
|
||||
let templates_folder = get_env_or("TEMPLATES_FOLDER", format!("{}/{}", &df, "templates"));
|
||||
|
||||
let cfg = _Config {
|
||||
database_url: get_env_or("DATABASE_URL", format!("{}/{}", &df, "db.sqlite3")),
|
||||
icon_cache_folder: get_env_or("ICON_CACHE_FOLDER", format!("{}/{}", &df, "icon_cache")),
|
||||
attachments_folder: get_env_or("ATTACHMENTS_FOLDER", format!("{}/{}", &df, "attachments")),
|
||||
_templates: load_templates(&templates_folder),
|
||||
templates_folder,
|
||||
reload_templates: get_env_or("RELOAD_TEMPLATES", false),
|
||||
|
||||
// icon_cache_ttl defaults to 30 days (30 * 24 * 60 * 60 seconds)
|
||||
icon_cache_ttl: get_env_or("ICON_CACHE_TTL", 2_592_000),
|
||||
// icon_cache_negttl defaults to 3 days (3 * 24 * 60 * 60 seconds)
|
||||
icon_cache_negttl: get_env_or("ICON_CACHE_NEGTTL", 259_200),
|
||||
|
||||
private_rsa_key: format!("{}.der", &key),
|
||||
private_rsa_key_pem: format!("{}.pem", &key),
|
||||
public_rsa_key: format!("{}.pub.der", &key),
|
||||
|
||||
web_vault_folder: get_env_or("WEB_VAULT_FOLDER", "web-vault/".into()),
|
||||
web_vault_enabled: get_env_or("WEB_VAULT_ENABLED", true),
|
||||
|
||||
websocket_enabled: get_env_or("WEBSOCKET_ENABLED", false),
|
||||
websocket_url: format!(
|
||||
"{}:{}",
|
||||
get_env_or("WEBSOCKET_ADDRESS", "0.0.0.0".to_string()),
|
||||
get_env_or("WEBSOCKET_PORT", 3012)
|
||||
),
|
||||
|
||||
extended_logging: get_env_or("EXTENDED_LOGGING", true),
|
||||
log_file: get_env("LOG_FILE"),
|
||||
|
||||
disable_icon_download: get_env_or("DISABLE_ICON_DOWNLOAD", false),
|
||||
signups_allowed: get_env_or("SIGNUPS_ALLOWED", true),
|
||||
admin_token: get_env("ADMIN_TOKEN"),
|
||||
invitations_allowed: get_env_or("INVITATIONS_ALLOWED", true),
|
||||
password_iterations: get_env_or("PASSWORD_ITERATIONS", 100_000),
|
||||
show_password_hint: get_env_or("SHOW_PASSWORD_HINT", true),
|
||||
|
||||
domain_set: domain.is_some(),
|
||||
domain: domain.unwrap_or("http://localhost".into()),
|
||||
|
||||
yubico_cred_set: yubico_client_id.is_some() && yubico_secret_key.is_some(),
|
||||
yubico_client_id: yubico_client_id.unwrap_or("00000".into()),
|
||||
yubico_secret_key: yubico_secret_key.unwrap_or("AAAAAAA".into()),
|
||||
yubico_server: get_env("YUBICO_SERVER"),
|
||||
|
||||
mail: MailConfig::load(),
|
||||
};
|
||||
|
||||
Config {
|
||||
inner: RwLock::new(cfg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MailConfig {
|
||||
pub smtp_host: String,
|
||||
pub smtp_port: u16,
|
||||
pub smtp_ssl: bool,
|
||||
pub smtp_from: String,
|
||||
pub smtp_from_name: String,
|
||||
pub smtp_username: Option<String>,
|
||||
pub smtp_password: Option<String>,
|
||||
}
|
||||
|
||||
impl MailConfig {
|
||||
fn load() -> Option<Self> {
|
||||
use crate::util::{get_env, get_env_or};
|
||||
|
||||
// When SMTP_HOST is absent, we assume the user does not want to enable it.
|
||||
let smtp_host = match get_env("SMTP_HOST") {
|
||||
Some(host) => host,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let smtp_from = get_env("SMTP_FROM").unwrap_or_else(|| {
|
||||
error!("Please specify SMTP_FROM to enable SMTP support.");
|
||||
exit(1);
|
||||
});
|
||||
|
||||
let smtp_from_name = get_env_or("SMTP_FROM_NAME", "Bitwarden_RS".into());
|
||||
|
||||
let smtp_ssl = get_env_or("SMTP_SSL", true);
|
||||
let smtp_port = get_env("SMTP_PORT").unwrap_or_else(|| if smtp_ssl { 587u16 } else { 25u16 });
|
||||
|
||||
let smtp_username = get_env("SMTP_USERNAME");
|
||||
let smtp_password = get_env("SMTP_PASSWORD").or_else(|| {
|
||||
if smtp_username.as_ref().is_some() {
|
||||
error!("SMTP_PASSWORD is mandatory when specifying SMTP_USERNAME.");
|
||||
exit(1);
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Some(MailConfig {
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
smtp_ssl,
|
||||
smtp_from,
|
||||
smtp_from_name,
|
||||
smtp_username,
|
||||
smtp_password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
34
src/mail.rs
34
src/mail.rs
@ -6,25 +6,26 @@ use native_tls::{Protocol, TlsConnector};
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::auth::{encode_jwt, generate_invite_claims};
|
||||
use crate::config::MailConfig;
|
||||
use crate::error::Error;
|
||||
use crate::CONFIG;
|
||||
|
||||
fn mailer(config: &MailConfig) -> SmtpTransport {
|
||||
let client_security = if config.smtp_ssl {
|
||||
fn mailer() -> SmtpTransport {
|
||||
let host = CONFIG.smtp_host().unwrap();
|
||||
|
||||
let client_security = if CONFIG.smtp_ssl() {
|
||||
let tls = TlsConnector::builder()
|
||||
.min_protocol_version(Some(Protocol::Tlsv11))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
ClientSecurity::Required(ClientTlsParameters::new(config.smtp_host.clone(), tls))
|
||||
ClientSecurity::Required(ClientTlsParameters::new(host.clone(), tls))
|
||||
} else {
|
||||
ClientSecurity::None
|
||||
};
|
||||
|
||||
let smtp_client = SmtpClient::new((config.smtp_host.as_str(), config.smtp_port), client_security).unwrap();
|
||||
let smtp_client = SmtpClient::new((host.as_str(), CONFIG.smtp_port()), client_security).unwrap();
|
||||
|
||||
let smtp_client = match (&config.smtp_username, &config.smtp_password) {
|
||||
let smtp_client = match (&CONFIG.smtp_username(), &CONFIG.smtp_password()) {
|
||||
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user.clone(), pass.clone())),
|
||||
_ => smtp_client,
|
||||
};
|
||||
@ -52,7 +53,7 @@ fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(Str
|
||||
Ok((subject, body))
|
||||
}
|
||||
|
||||
pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConfig) -> EmptyResult {
|
||||
pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
|
||||
let template_name = if hint.is_some() {
|
||||
"email/pw_hint_some"
|
||||
} else {
|
||||
@ -61,7 +62,7 @@ pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConf
|
||||
|
||||
let (subject, body) = get_text(template_name, json!({ "hint": hint }))?;
|
||||
|
||||
send_email(&address, &subject, &body, &config)
|
||||
send_email(&address, &subject, &body)
|
||||
}
|
||||
|
||||
pub fn send_invite(
|
||||
@ -71,7 +72,6 @@ pub fn send_invite(
|
||||
org_user_id: Option<String>,
|
||||
org_name: &str,
|
||||
invited_by_email: Option<String>,
|
||||
config: &MailConfig,
|
||||
) -> EmptyResult {
|
||||
let claims = generate_invite_claims(
|
||||
uuid.to_string(),
|
||||
@ -94,10 +94,10 @@ pub fn send_invite(
|
||||
}),
|
||||
)?;
|
||||
|
||||
send_email(&address, &subject, &body, &config)
|
||||
send_email(&address, &subject, &body)
|
||||
}
|
||||
|
||||
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str, config: &MailConfig) -> EmptyResult {
|
||||
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
|
||||
let (subject, body) = get_text(
|
||||
"email/invite_accepted",
|
||||
json!({
|
||||
@ -107,10 +107,10 @@ pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str,
|
||||
}),
|
||||
)?;
|
||||
|
||||
send_email(&address, &subject, &body, &config)
|
||||
send_email(&address, &subject, &body)
|
||||
}
|
||||
|
||||
pub fn send_invite_confirmed(address: &str, org_name: &str, config: &MailConfig) -> EmptyResult {
|
||||
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
||||
let (subject, body) = get_text(
|
||||
"email/invite_confirmed",
|
||||
json!({
|
||||
@ -119,20 +119,20 @@ pub fn send_invite_confirmed(address: &str, org_name: &str, config: &MailConfig)
|
||||
}),
|
||||
)?;
|
||||
|
||||
send_email(&address, &subject, &body, &config)
|
||||
send_email(&address, &subject, &body)
|
||||
}
|
||||
|
||||
fn send_email(address: &str, subject: &str, body: &str, config: &MailConfig) -> EmptyResult {
|
||||
fn send_email(address: &str, subject: &str, body: &str) -> EmptyResult {
|
||||
let email = EmailBuilder::new()
|
||||
.to(address)
|
||||
.from((config.smtp_from.as_str(), config.smtp_from_name.as_str()))
|
||||
.from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str()))
|
||||
.subject(subject)
|
||||
.header(("Content-Type", "text/html"))
|
||||
.body(body)
|
||||
.build()
|
||||
.map_err(|e| Error::new("Error building email", e.to_string()))?;
|
||||
|
||||
mailer(config)
|
||||
mailer()
|
||||
.send(email.into())
|
||||
.map_err(|e| Error::new("Error sending email", e.to_string()))
|
||||
.and(Ok(()))
|
||||
|
41
src/util.rs
41
src/util.rs
@ -140,18 +140,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_parse_string_or<S, T, U>(string: impl Try<Ok = S, Error = U>, default: T) -> T
|
||||
where
|
||||
S: AsRef<str>,
|
||||
T: FromStr,
|
||||
{
|
||||
if let Ok(Ok(value)) = string.into_result().map(|s| s.as_ref().parse::<T>()) {
|
||||
value
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Env methods
|
||||
//
|
||||
@ -165,13 +153,6 @@ where
|
||||
try_parse_string(env::var(key))
|
||||
}
|
||||
|
||||
pub fn get_env_or<V>(key: &str, default: V) -> V
|
||||
where
|
||||
V: FromStr,
|
||||
{
|
||||
try_parse_string_or(env::var(key), default)
|
||||
}
|
||||
|
||||
//
|
||||
// Date util methods
|
||||
//
|
||||
@ -303,3 +284,25 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Into Result
|
||||
//
|
||||
use crate::error::Error;
|
||||
|
||||
pub trait IntoResult<T> {
|
||||
fn into_result(self) -> Result<T, Error>;
|
||||
}
|
||||
|
||||
impl<T> IntoResult<T> for Result<T, Error> {
|
||||
fn into_result(self) -> Result<T, Error> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResult<T> for T {
|
||||
fn into_result(self) -> Result<T, Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user