geforkt von mirrored/vaultwarden
Config can now be serialized / deserialized
Dieser Commit ist enthalten in:
Ursprung
20d8d800f3
Commit
86ed75bf7c
8 geänderte Dateien mit 207 neuen und 261 gelöschten Zeilen
|
@ -24,6 +24,8 @@ pub fn routes() -> Vec<Route> {
|
||||||
invite_user,
|
invite_user,
|
||||||
delete_user,
|
delete_user,
|
||||||
deauth_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")
|
err!("Invitations are not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mail_config) = CONFIG.mail() {
|
if CONFIG.mail_enabled() {
|
||||||
let mut user = User::new(email);
|
let mut user = User::new(email);
|
||||||
user.save(&conn)?;
|
user.save(&conn)?;
|
||||||
let org_name = "bitwarden_rs";
|
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 {
|
} else {
|
||||||
let mut invitation = Invitation::new(data.Email);
|
let mut invitation = Invitation::new(data.Email);
|
||||||
invitation.save(&conn)
|
invitation.save(&conn)
|
||||||
|
@ -169,6 +171,20 @@ fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
user.save(&conn)
|
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 {}
|
pub struct AdminToken {}
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for 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(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref mail_config) = CONFIG.mail() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_password_hint(&data.Email, hint, mail_config)?;
|
mail::send_password_hint(&data.Email, hint)?;
|
||||||
} else if CONFIG.show_password_hint() {
|
} else if CONFIG.show_password_hint() {
|
||||||
if let Some(hint) = hint {
|
if let Some(hint) = hint {
|
||||||
err!(format!("Your password hint is: {}", &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() {
|
for email in data.Emails.iter() {
|
||||||
let mut user_org_status = match CONFIG.mail() {
|
let mut user_org_status = match CONFIG.mail_enabled() {
|
||||||
Some(_) => UserOrgStatus::Invited as i32,
|
true => UserOrgStatus::Invited as i32,
|
||||||
None => UserOrgStatus::Accepted as i32, // Automatically mark user as accepted if no email invites
|
false => UserOrgStatus::Accepted as i32, // Automatically mark user as accepted if no email invites
|
||||||
};
|
};
|
||||||
let user = match User::find_by_mail(&email, &conn) {
|
let user = match User::find_by_mail(&email, &conn) {
|
||||||
None => {
|
None => {
|
||||||
|
@ -496,7 +496,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||||
err!(format!("User email does not exist: {}", email))
|
err!(format!("User email does not exist: {}", email))
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail().is_none() {
|
if !CONFIG.mail_enabled() {
|
||||||
let mut invitation = Invitation::new(email.clone());
|
let mut invitation = Invitation::new(email.clone());
|
||||||
invitation.save(&conn)?;
|
invitation.save(&conn)?;
|
||||||
}
|
}
|
||||||
|
@ -535,7 +535,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||||
|
|
||||||
new_user.save(&conn)?;
|
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) {
|
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
||||||
Some(org) => org.name,
|
Some(org) => org.name,
|
||||||
None => err!("Error looking up organization"),
|
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),
|
Some(new_user.uuid),
|
||||||
&org_name,
|
&org_name,
|
||||||
Some(headers.user.email.clone()),
|
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.")
|
err!("Invitations are not allowed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail().is_none() {
|
if !CONFIG.mail_enabled() {
|
||||||
err!("SMTP is not configured.")
|
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."),
|
None => err!("Error looking up organization."),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref mail_config) = CONFIG.mail() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_invite(
|
mail::send_invite(
|
||||||
&user.email,
|
&user.email,
|
||||||
&user.uuid,
|
&user.uuid,
|
||||||
|
@ -593,7 +592,6 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
|
||||||
Some(user_org.uuid),
|
Some(user_org.uuid),
|
||||||
&org_name,
|
&org_name,
|
||||||
Some(headers.user.email),
|
Some(headers.user.email),
|
||||||
mail_config,
|
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
let mut invitation = Invitation::new(user.email.clone());
|
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"),
|
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");
|
let mut org_name = String::from("bitwarden_rs");
|
||||||
if let Some(org_id) = &claims.org_id {
|
if let Some(org_id) = &claims.org_id {
|
||||||
org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
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 {
|
if let Some(invited_by_email) = &claims.invited_by_email {
|
||||||
// User was invited to an organization, so they must be confirmed manually after acceptance
|
// 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 {
|
} else {
|
||||||
// User was invited from /admin, so they are automatically confirmed
|
// 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"),
|
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) {
|
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
||||||
Some(org) => org.name,
|
Some(org) => org.name,
|
||||||
None => err!("Error looking up organization."),
|
None => err!("Error looking up organization."),
|
||||||
|
@ -695,7 +693,7 @@ fn confirm_invite(
|
||||||
Some(user) => user.email,
|
Some(user) => user.email,
|
||||||
None => err!("Error looking up user."),
|
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)
|
user_to_confirm.save(&conn)
|
||||||
|
|
|
@ -3,15 +3,14 @@ use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||||
|
use crate::auth::Headers;
|
||||||
|
use crate::crypto;
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
models::{TwoFactor, TwoFactorType, User},
|
models::{TwoFactor, TwoFactorType, User},
|
||||||
DbConn,
|
DbConn,
|
||||||
};
|
};
|
||||||
|
use crate::error::{Error, MapResult};
|
||||||
use crate::crypto;
|
|
||||||
|
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
|
||||||
use crate::auth::Headers;
|
|
||||||
|
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
|
|
||||||
|
@ -508,32 +507,31 @@ fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_yubikey_otp(otp: String) -> JsonResult {
|
fn get_yubico_credentials() -> Result<(String, String), Error> {
|
||||||
if !CONFIG.yubico_cred_set() {
|
match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
|
||||||
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled")
|
(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 yubico = Yubico::new();
|
||||||
let config = Config::default()
|
let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
|
||||||
.set_client_id(CONFIG.yubico_client_id())
|
|
||||||
.set_key(CONFIG.yubico_secret_key());
|
|
||||||
|
|
||||||
let result = match CONFIG.yubico_server() {
|
match CONFIG.yubico_server() {
|
||||||
Some(server) => yubico.verify(otp, config.set_api_hosts(vec![server])),
|
Some(server) => yubico.verify(otp, config.set_api_hosts(vec![server])),
|
||||||
None => yubico.verify(otp, config),
|
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>")]
|
#[post("/two-factor/get-yubikey", data = "<data>")]
|
||||||
fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
if !CONFIG.yubico_cred_set() {
|
// Make sure the credentials are set
|
||||||
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled")
|
get_yubico_credentials()?;
|
||||||
}
|
|
||||||
|
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
@ -597,11 +595,7 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = verify_yubikey_otp(yubikey.to_owned());
|
verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
|
||||||
|
|
||||||
if let Err(_e) = result {
|
|
||||||
err!("Invalid Yubikey OTP provided");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
|
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 || {
|
thread::spawn(move || {
|
||||||
WebSocket::new(factory)
|
WebSocket::new(factory)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.listen(&CONFIG.websocket_url())
|
.listen((CONFIG.websocket_address().as_str(), CONFIG.websocket_port()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
285
src/config.rs
285
src/config.rs
|
@ -3,77 +3,149 @@ use std::sync::RwLock;
|
||||||
|
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::util::IntoResult;
|
||||||
|
|
||||||
lazy_static! {
|
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 {
|
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 Inner {
|
||||||
struct _Config {
|
templates: Handlebars,
|
||||||
_templates: Handlebars,
|
config: _Config,
|
||||||
$(pub $name: $ty),+
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
|
struct _Config { $(pub $name: $ty),+ }
|
||||||
|
|
||||||
paste::item! {
|
paste::item! {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl Config {
|
impl Config {
|
||||||
$(
|
$(
|
||||||
pub fn $name(&self) -> $ty {
|
pub fn $name(&self) -> $ty {
|
||||||
self.inner.read().unwrap().$name.clone()
|
self.inner.read().unwrap().config.$name.clone()
|
||||||
}
|
}
|
||||||
pub fn [<set_ $name>](&self, value: $ty) {
|
pub fn [<set_ $name>](&self, value: $ty) {
|
||||||
self.inner.write().unwrap().$name = value;
|
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();
|
||||||
|
|
||||||
|
$(
|
||||||
|
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! {
|
make_config! {
|
||||||
database_url: String,
|
data_folder: String, |_| "data".to_string();
|
||||||
icon_cache_folder: String,
|
database_url: String, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
|
||||||
attachments_folder: String,
|
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,
|
rsa_key_filename: String, |c| format!("{}/{}", c.data_folder, "rsa_key");
|
||||||
icon_cache_negttl: u64,
|
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,
|
websocket_enabled: bool, |_| false;
|
||||||
private_rsa_key_pem: String,
|
websocket_address: String, |_| "0.0.0.0".to_string();
|
||||||
public_rsa_key: String,
|
websocket_port: u16, |_| 3012;
|
||||||
|
|
||||||
web_vault_folder: String,
|
web_vault_folder: String, |_| "web-vault/".to_string();
|
||||||
web_vault_enabled: bool,
|
web_vault_enabled: bool, |_| true;
|
||||||
|
|
||||||
websocket_enabled: bool,
|
icon_cache_ttl: u64, |_| 2_592_000;
|
||||||
websocket_url: String,
|
icon_cache_negttl: u64, |_| 259_200;
|
||||||
|
|
||||||
extended_logging: bool,
|
disable_icon_download: bool, |_| false;
|
||||||
log_file: Option<String>,
|
signups_allowed: bool, |_| true;
|
||||||
|
invitations_allowed: bool, |_| true;
|
||||||
|
password_iterations: i32, |_| 100_000;
|
||||||
|
show_password_hint: bool, |_| true;
|
||||||
|
|
||||||
disable_icon_download: bool,
|
domain: String, |_| "http://localhost".to_string();
|
||||||
signups_allowed: bool,
|
domain_set: bool, |_| false;
|
||||||
invitations_allowed: bool,
|
|
||||||
admin_token: Option<String>,
|
|
||||||
password_iterations: i32,
|
|
||||||
show_password_hint: bool,
|
|
||||||
|
|
||||||
domain: String,
|
reload_templates: bool, |_| false;
|
||||||
domain_set: bool,
|
|
||||||
|
|
||||||
yubico_cred_set: bool,
|
extended_logging: bool, |_| true;
|
||||||
yubico_client_id: String,
|
log_file: Option<String>;
|
||||||
yubico_secret_key: String,
|
|
||||||
yubico_server: Option<String>,
|
|
||||||
|
|
||||||
mail: Option<MailConfig>,
|
admin_token: Option<String>;
|
||||||
templates_folder: String,
|
|
||||||
reload_templates: bool,
|
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 {
|
fn load_templates(path: &str) -> Handlebars {
|
||||||
|
@ -106,140 +178,3 @@ fn load_templates(path: &str) -> Handlebars {
|
||||||
|
|
||||||
hb
|
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::api::EmptyResult;
|
||||||
use crate::auth::{encode_jwt, generate_invite_claims};
|
use crate::auth::{encode_jwt, generate_invite_claims};
|
||||||
use crate::config::MailConfig;
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
fn mailer(config: &MailConfig) -> SmtpTransport {
|
fn mailer() -> SmtpTransport {
|
||||||
let client_security = if config.smtp_ssl {
|
let host = CONFIG.smtp_host().unwrap();
|
||||||
|
|
||||||
|
let client_security = if CONFIG.smtp_ssl() {
|
||||||
let tls = TlsConnector::builder()
|
let tls = TlsConnector::builder()
|
||||||
.min_protocol_version(Some(Protocol::Tlsv11))
|
.min_protocol_version(Some(Protocol::Tlsv11))
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
ClientSecurity::Required(ClientTlsParameters::new(config.smtp_host.clone(), tls))
|
ClientSecurity::Required(ClientTlsParameters::new(host.clone(), tls))
|
||||||
} else {
|
} else {
|
||||||
ClientSecurity::None
|
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())),
|
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user.clone(), pass.clone())),
|
||||||
_ => smtp_client,
|
_ => smtp_client,
|
||||||
};
|
};
|
||||||
|
@ -52,7 +53,7 @@ fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(Str
|
||||||
Ok((subject, body))
|
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() {
|
let template_name = if hint.is_some() {
|
||||||
"email/pw_hint_some"
|
"email/pw_hint_some"
|
||||||
} else {
|
} 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 }))?;
|
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(
|
pub fn send_invite(
|
||||||
|
@ -71,7 +72,6 @@ pub fn send_invite(
|
||||||
org_user_id: Option<String>,
|
org_user_id: Option<String>,
|
||||||
org_name: &str,
|
org_name: &str,
|
||||||
invited_by_email: Option<String>,
|
invited_by_email: Option<String>,
|
||||||
config: &MailConfig,
|
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
let claims = generate_invite_claims(
|
let claims = generate_invite_claims(
|
||||||
uuid.to_string(),
|
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(
|
let (subject, body) = get_text(
|
||||||
"email/invite_accepted",
|
"email/invite_accepted",
|
||||||
json!({
|
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(
|
let (subject, body) = get_text(
|
||||||
"email/invite_confirmed",
|
"email/invite_confirmed",
|
||||||
json!({
|
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()
|
let email = EmailBuilder::new()
|
||||||
.to(address)
|
.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)
|
.subject(subject)
|
||||||
.header(("Content-Type", "text/html"))
|
.header(("Content-Type", "text/html"))
|
||||||
.body(body)
|
.body(body)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| Error::new("Error building email", e.to_string()))?;
|
.map_err(|e| Error::new("Error building email", e.to_string()))?;
|
||||||
|
|
||||||
mailer(config)
|
mailer()
|
||||||
.send(email.into())
|
.send(email.into())
|
||||||
.map_err(|e| Error::new("Error sending email", e.to_string()))
|
.map_err(|e| Error::new("Error sending email", e.to_string()))
|
||||||
.and(Ok(()))
|
.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
|
// Env methods
|
||||||
//
|
//
|
||||||
|
@ -165,13 +153,6 @@ where
|
||||||
try_parse_string(env::var(key))
|
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
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Laden …
In neuem Issue referenzieren