2020-03-09 21:04:03 +00:00
use once_cell ::sync ::Lazy ;
2019-01-25 17:23:51 +00:00
use std ::process ::exit ;
use std ::sync ::RwLock ;
2020-02-19 05:27:00 +00:00
use reqwest ::Url ;
2019-02-02 00:09:21 +00:00
use crate ::error ::Error ;
2020-01-20 21:28:54 +00:00
use crate ::util ::{ get_env , get_env_bool } ;
2019-02-02 00:09:21 +00:00
2020-03-09 21:04:03 +00:00
static CONFIG_FILE : Lazy < String > = Lazy ::new ( | | {
let data_folder = get_env ( " DATA_FOLDER " ) . unwrap_or_else ( | | String ::from ( " data " ) ) ;
get_env ( " CONFIG_FILE " ) . unwrap_or_else ( | | format! ( " {} /config.json " , data_folder ) )
} ) ;
pub static CONFIG : Lazy < Config > = Lazy ::new ( | | {
Config ::load ( ) . unwrap_or_else ( | e | {
2019-02-02 00:09:21 +00:00
println! ( " Error loading config: \n \t {:?} \n " , e ) ;
exit ( 12 )
2020-03-09 21:04:03 +00:00
} )
} ) ;
2019-01-25 17:23:51 +00:00
2019-02-08 19:49:04 +00:00
pub type Pass = String ;
2019-01-25 17:23:51 +00:00
macro_rules ! make_config {
2019-02-06 16:32:13 +00:00
( $(
$( #[ doc = $groupdoc:literal ] ) ?
$group :ident $( : $group_enabled :ident ) ? {
2019-02-05 21:17:02 +00:00
$(
2019-02-06 16:32:13 +00:00
$( #[ doc = $doc:literal ] ) +
2020-01-20 21:28:54 +00:00
$name :ident : $ty :ident , $editable :literal , $none_action :ident $(, $default :expr ) ? ;
2019-02-06 16:32:13 +00:00
) + } ,
) + ) = > {
2019-02-02 00:09:21 +00:00
pub struct Config { inner : RwLock < Inner > }
2019-01-25 17:23:51 +00:00
2019-02-02 00:09:21 +00:00
struct Inner {
2020-01-26 14:29:14 +00:00
templates : Handlebars < 'static > ,
2019-02-02 15:47:27 +00:00
config : ConfigItems ,
2019-02-02 23:22:18 +00:00
_env : ConfigBuilder ,
_usr : ConfigBuilder ,
2019-01-25 17:23:51 +00:00
}
2019-02-02 23:22:18 +00:00
#[ derive(Debug, Clone, Default, Deserialize, Serialize) ]
2019-02-02 15:47:27 +00:00
pub struct ConfigBuilder {
2019-02-05 21:17:02 +00:00
$( $(
2019-02-02 23:22:18 +00:00
#[ serde(skip_serializing_if = " Option::is_none " ) ]
2019-02-05 21:17:02 +00:00
$name : Option < $ty > ,
) + ) +
2019-02-02 15:47:27 +00:00
}
impl ConfigBuilder {
fn from_env ( ) -> Self {
2019-02-06 13:54:39 +00:00
dotenv ::from_path ( " .env " ) . ok ( ) ;
2019-02-02 15:47:27 +00:00
let mut builder = ConfigBuilder ::default ( ) ;
2019-02-05 21:17:02 +00:00
$( $(
2020-01-20 21:28:54 +00:00
builder . $name = make_config! { @ getenv & stringify! ( $name ) . to_uppercase ( ) , $ty } ;
2019-02-05 21:17:02 +00:00
) + ) +
2020-03-09 21:04:03 +00:00
2019-02-02 15:47:27 +00:00
builder
}
fn from_file ( path : & str ) -> Result < Self , Error > {
use crate ::util ::read_file_string ;
let config_str = read_file_string ( path ) ? ;
serde_json ::from_str ( & config_str ) . map_err ( Into ::into )
}
2019-02-02 23:22:18 +00:00
/// Merges the values of both builders into a new builder.
/// If both have the same element, `other` wins.
2019-04-11 14:08:26 +00:00
fn merge ( & self , other : & Self , show_overrides : bool ) -> Self {
2019-02-20 19:59:37 +00:00
let mut overrides = Vec ::new ( ) ;
2019-02-02 23:22:18 +00:00
let mut builder = self . clone ( ) ;
2019-02-05 21:17:02 +00:00
$( $(
2019-02-02 23:22:18 +00:00
if let v @ Some ( _ ) = & other . $name {
builder . $name = v . clone ( ) ;
2019-02-20 19:59:37 +00:00
if self . $name . is_some ( ) {
overrides . push ( stringify! ( $name ) . to_uppercase ( ) ) ;
}
2019-02-02 15:47:27 +00:00
}
2019-02-05 21:17:02 +00:00
) + ) +
2019-02-20 19:59:37 +00:00
2019-04-11 14:08:26 +00:00
if show_overrides & & ! overrides . is_empty ( ) {
2019-02-20 19:59:37 +00:00
// We can't use warn! here because logging isn't setup yet.
println! ( " [WARNING] The following environment variables are being overriden by the config file, " ) ;
println! ( " [WARNING] please use the admin panel to make changes to them: " ) ;
println! ( " [WARNING] {} \n " , overrides . join ( " , " ) ) ;
}
2019-02-02 23:22:18 +00:00
builder
2019-02-02 15:47:27 +00:00
}
2019-02-02 23:22:18 +00:00
/// Returns a new builder with all the elements from self,
/// except those that are equal in both sides
2019-02-08 17:45:07 +00:00
fn _remove ( & self , other : & Self ) -> Self {
2019-02-02 23:22:18 +00:00
let mut builder = ConfigBuilder ::default ( ) ;
2019-02-05 21:17:02 +00:00
$( $(
2019-02-02 23:22:18 +00:00
if & self . $name ! = & other . $name {
builder . $name = self . $name . clone ( ) ;
}
2019-02-05 21:17:02 +00:00
) + ) +
2019-02-02 23:22:18 +00:00
builder
}
fn build ( & self ) -> ConfigItems {
2019-02-02 15:47:27 +00:00
let mut config = ConfigItems ::default ( ) ;
let _domain_set = self . domain . is_some ( ) ;
2019-02-05 21:17:02 +00:00
$( $(
2019-02-02 23:22:18 +00:00
config . $name = make_config! { @ build self . $name . clone ( ) , & config , $none_action , $( $default ) ? } ;
2019-02-05 21:17:02 +00:00
) + ) +
2019-02-02 15:47:27 +00:00
config . domain_set = _domain_set ;
2020-04-09 08:42:27 +00:00
config . signups_domains_whitelist = config . signups_domains_whitelist . trim ( ) . to_lowercase ( ) ;
2019-02-02 15:47:27 +00:00
config
}
}
2019-02-02 23:22:18 +00:00
#[ derive(Debug, Clone, Default) ]
2019-02-05 21:17:02 +00:00
pub struct ConfigItems { $( $( pub $name : make_config ! { @ type $ty , $none_action } , ) + ) + }
2019-02-02 00:09:21 +00:00
#[ allow(unused) ]
impl Config {
2019-02-05 21:17:02 +00:00
$( $(
2019-02-02 23:22:18 +00:00
pub fn $name ( & self ) -> make_config ! { @ type $ty , $none_action } {
2019-02-02 00:09:21 +00:00
self . inner . read ( ) . unwrap ( ) . config . $name . clone ( )
}
2019-02-05 21:17:02 +00:00
) + ) +
2019-02-02 00:09:21 +00:00
2019-02-02 23:22:18 +00:00
pub fn prepare_json ( & self ) -> serde_json ::Value {
2019-02-05 21:17:02 +00:00
let ( def , cfg ) = {
2019-02-02 23:22:18 +00:00
let inner = & self . inner . read ( ) . unwrap ( ) ;
2019-02-05 21:17:02 +00:00
( inner . _env . build ( ) , inner . config . clone ( ) )
2019-02-02 23:22:18 +00:00
} ;
fn _get_form_type ( rust_type : & str ) -> & 'static str {
match rust_type {
2019-02-08 19:49:04 +00:00
" Pass " = > " password " ,
2019-02-02 23:22:18 +00:00
" String " = > " text " ,
" bool " = > " checkbox " ,
_ = > " number "
}
}
2019-02-02 15:47:27 +00:00
2019-02-04 00:37:25 +00:00
fn _get_doc ( doc : & str ) -> serde_json ::Value {
let mut split = doc . split ( " |> " ) . map ( str ::trim ) ;
json! ( {
" name " : split . next ( ) ,
" description " : split . next ( )
} )
}
2019-02-05 21:17:02 +00:00
json! ( [ $( {
" group " : stringify ! ( $group ) ,
2019-02-05 23:38:57 +00:00
" grouptoggle " : stringify ! ( $( $group_enabled ) ? ) ,
2019-02-05 21:17:02 +00:00
" groupdoc " : make_config ! { @ show $( $groupdoc ) ? } ,
" elements " : [
$( {
" editable " : $editable ,
" name " : stringify ! ( $name ) ,
" value " : cfg . $name ,
" default " : def . $name ,
" type " : _get_form_type ( stringify! ( $ty ) ) ,
" doc " : _get_doc ( concat! ( $( $doc ) , + ) ) ,
} , ) +
] } , ) + ] )
2019-02-02 23:22:18 +00:00
}
2019-02-02 15:47:27 +00:00
}
} ;
2019-02-05 21:17:02 +00:00
// Group or empty string
( @ show ) = > { " " } ;
2019-02-05 23:38:57 +00:00
( @ show $lit :literal ) = > { $lit } ;
2019-02-05 21:17:02 +00:00
2019-02-02 23:22:18 +00:00
// Wrap the optionals in an Option type
( @ type $ty :ty , option ) = > { Option < $ty > } ;
( @ type $ty :ty , $id :ident ) = > { $ty } ;
// Generate the values depending on none_action
( @ build $value :expr , $config :expr , option , ) = > { $value } ;
( @ build $value :expr , $config :expr , def , $default :expr ) = > { $value . unwrap_or ( $default ) } ;
( @ build $value :expr , $config :expr , auto , $default_fn :expr ) = > { {
2019-02-02 15:47:27 +00:00
match $value {
2019-02-02 00:09:21 +00:00
Some ( v ) = > v ,
None = > {
2019-06-01 22:28:20 +00:00
let f : & dyn Fn ( & ConfigItems ) -> _ = & $default_fn ;
2019-02-02 15:47:27 +00:00
f ( $config )
2019-02-02 00:09:21 +00:00
}
}
2019-02-02 23:22:18 +00:00
} } ;
2019-12-27 17:42:39 +00:00
( @ build $value :expr , $config :expr , gen , $default_fn :expr ) = > { {
let f : & dyn Fn ( & ConfigItems ) -> _ = & $default_fn ;
f ( $config )
} } ;
2020-01-20 21:28:54 +00:00
( @ getenv $name :expr , bool ) = > { get_env_bool ( $name ) } ;
( @ getenv $name :expr , $ty :ident ) = > { get_env ( $name ) } ;
2019-02-02 00:09:21 +00:00
}
2019-01-25 17:23:51 +00:00
2019-02-04 00:37:25 +00:00
//STRUCTURE:
2019-02-05 21:17:02 +00:00
// /// Short description (without this they won't appear on the list)
// group {
// /// Friendly Name |> Description (Optional)
2019-12-27 17:42:39 +00:00
// name: type, is_editable, action, <default_value (Optional)>
2019-02-05 21:17:02 +00:00
// }
2019-02-04 00:37:25 +00:00
//
2019-12-27 17:42:39 +00:00
// Where action applied when the value wasn't provided and can be:
2019-02-02 23:22:18 +00:00
// def: Use a default value
// auto: Value is auto generated based on other values
// option: Value is optional
2019-12-27 17:42:39 +00:00
// gen: Value is always autogenerated and it's original value ignored
2019-02-02 00:09:21 +00:00
make_config! {
2019-02-05 21:17:02 +00:00
folders {
/// Data folder |> Main data folder
data_folder : String , false , def , " data " . to_string ( ) ;
2019-05-26 21:02:41 +00:00
/// Database URL
database_url : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " db.sqlite3 " ) ;
2019-06-18 14:45:19 +00:00
/// Icon cache folder
2019-02-05 21:17:02 +00:00
icon_cache_folder : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " icon_cache " ) ;
/// Attachments folder
attachments_folder : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " attachments " ) ;
/// Templates folder
templates_folder : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " templates " ) ;
/// Session JWT key
rsa_key_filename : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " rsa_key " ) ;
/// Web vault folder
web_vault_folder : String , false , def , " web-vault/ " . to_string ( ) ;
2019-02-06 16:32:13 +00:00
} ,
2019-02-05 21:17:02 +00:00
ws {
/// Enable websocket notifications
websocket_enabled : bool , false , def , false ;
/// Websocket address
websocket_address : String , false , def , " 0.0.0.0 " . to_string ( ) ;
/// Websocket port
websocket_port : u16 , false , def , 3012 ;
} ,
2019-02-06 16:32:13 +00:00
2019-02-05 21:17:02 +00:00
/// General settings
settings {
2019-03-03 15:09:15 +00:00
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://'
/// and port, if it's different than the default. Some server functions don't work correctly without this value
2019-02-05 21:17:02 +00:00
domain : String , true , def , " http://localhost " . to_string ( ) ;
2019-02-18 18:25:33 +00:00
/// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.
2019-02-05 21:17:02 +00:00
domain_set : bool , false , def , false ;
2020-02-19 05:27:00 +00:00
/// Domain origin |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin)
domain_origin : String , false , auto , | c | extract_url_origin ( & c . domain ) ;
/// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path)
domain_path : String , false , auto , | c | extract_url_path ( & c . domain ) ;
2019-02-05 21:17:02 +00:00
/// Enable web vault
web_vault_enabled : bool , false , def , true ;
2019-08-20 18:07:12 +00:00
/// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key
2019-08-20 21:53:00 +00:00
hibp_api_key : Pass , true , option ;
2019-08-20 18:07:12 +00:00
2020-02-17 21:56:26 +00:00
/// Per-user attachment limit (KB) |> Limit in kilobytes for a users attachments, once the limit is exceeded it won't be possible to upload more
user_attachment_limit : i64 , true , option ;
/// Per-organization attachment limit (KB) |> Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more
org_attachment_limit : i64 , true , option ;
2019-03-03 15:09:15 +00:00
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
2019-02-05 21:17:02 +00:00
/// otherwise it will delete them and they won't be downloaded again.
disable_icon_download : bool , true , def , false ;
2020-04-09 08:42:27 +00:00
/// Allow new signups |> Controls whether new users can register. Users can be invited by the bitwarden_rs admin even if this is disabled
2019-02-05 21:17:02 +00:00
signups_allowed : bool , true , def , true ;
2019-11-25 05:28:49 +00:00
/// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
signups_verify : bool , true , def , false ;
/// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
signups_verify_resend_time : u64 , true , def , 3_600 ;
/// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)
signups_verify_resend_limit : u32 , true , def , 6 ;
2020-04-09 08:42:27 +00:00
/// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled
2019-11-16 22:01:45 +00:00
signups_domains_whitelist : String , true , def , " " . to_string ( ) ;
2020-04-09 08:42:27 +00:00
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are otherwise disabled
2019-02-05 21:17:02 +00:00
invitations_allowed : bool , true , def , true ;
2019-03-03 15:09:15 +00:00
/// Password iterations |> Number of server-side passwords hashing iterations.
/// The changes only apply when a user changes their password. Not recommended to lower the value
2019-02-05 21:17:02 +00:00
password_iterations : i32 , true , def , 100_000 ;
2019-03-03 15:09:15 +00:00
/// Show password hints |> Controls if the password hint should be shown directly in the web page.
/// Otherwise, if email is disabled, there is no way to see the password hint
2019-02-05 21:17:02 +00:00
show_password_hint : bool , true , def , true ;
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
2019-02-08 19:49:04 +00:00
admin_token : Pass , true , option ;
2020-02-04 21:14:50 +00:00
/// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
invitation_org_name : String , true , def , " Bitwarden_RS " . to_string ( ) ;
2019-02-05 21:17:02 +00:00
} ,
/// Advanced settings
2019-02-06 16:32:13 +00:00
advanced {
2019-12-27 17:42:39 +00:00
/// Client IP header |> If not present, the remote IP is used.
/// Set to the string "none" (without quotes), to disable any headers and just use the remote IP
ip_header : String , true , def , " X-Real-IP " . to_string ( ) ;
/// Internal IP header property, used to avoid recomputing each time
_ip_header_enabled : bool , false , gen , | c | & c . ip_header . trim ( ) . to_lowercase ( ) ! = " none " ;
2019-02-05 21:17:02 +00:00
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
icon_cache_ttl : u64 , true , def , 2_592_000 ;
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
icon_cache_negttl : u64 , true , def , 259_200 ;
2019-02-12 20:56:28 +00:00
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
2019-03-03 15:09:15 +00:00
icon_download_timeout : u64 , true , def , 10 ;
2019-03-18 21:12:39 +00:00
/// Icon blacklist Regex |> Any domains or IPs that match this regex won't be fetched by the icon service.
/// Useful to hide other servers in the local network. Check the WIKI for more details
icon_blacklist_regex : String , true , option ;
2019-10-05 12:48:15 +00:00
/// Icon blacklist non global IPs |> Any IP which is not defined as a global IP will be blacklisted.
/// Usefull to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
icon_blacklist_non_global_ips : bool , true , def , true ;
2019-02-05 21:17:02 +00:00
2019-03-03 15:09:15 +00:00
/// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
/// Note that the checkbox would still be present, but ignored.
disable_2fa_remember : bool , true , def , false ;
2019-02-05 21:17:02 +00:00
2019-11-07 16:11:29 +00:00
/// Disable authenticator time drifted codes to be valid |> Enabling this only allows the current TOTP code to be valid
/// TOTP codes of the previous and next 30 seconds will be invalid.
2020-02-04 21:14:50 +00:00
authenticator_disable_time_drift : bool , true , def , false ;
2019-11-07 16:11:29 +00:00
2019-08-19 20:14:00 +00:00
/// Require new device emails |> When a user logs in an email is required to be sent.
/// If sending the email fails the login attempt will fail.
require_device_email : bool , true , def , false ;
2019-03-03 15:09:15 +00:00
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
/// ONLY use this during development, as it can slow down the server
2019-02-05 21:17:02 +00:00
reload_templates : bool , true , def , false ;
/// Enable extended logging
extended_logging : bool , false , def , true ;
2019-03-29 19:27:20 +00:00
/// Enable the log to output to Syslog
use_syslog : bool , false , def , false ;
2019-02-05 21:17:02 +00:00
/// Log file path
log_file : String , false , option ;
2019-03-25 13:12:41 +00:00
/// Log level
log_level : String , false , def , " Info " . to_string ( ) ;
2019-02-18 10:48:48 +00:00
2019-03-03 15:09:15 +00:00
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using bitwarden_rs on some exotic filesystems,
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
2019-02-18 10:48:48 +00:00
enable_db_wal : bool , false , def , true ;
2019-02-20 20:44:35 +00:00
2019-10-08 17:33:27 +00:00
/// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
2019-02-20 20:44:35 +00:00
disable_admin_token : bool , true , def , false ;
2020-02-04 21:14:50 +00:00
/// Allowed iframe ancestors (Know the risks!) |> Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets
allowed_iframe_ancestors : String , true , def , String ::new ( ) ;
2019-02-05 21:17:02 +00:00
} ,
/// Yubikey settings
2019-02-05 23:38:57 +00:00
yubico : _enable_yubico {
/// Enabled
_enable_yubico : bool , true , def , true ;
2019-02-05 21:17:02 +00:00
/// Client ID
yubico_client_id : String , true , option ;
/// Secret Key
2019-02-08 19:49:04 +00:00
yubico_secret_key : Pass , true , option ;
2019-02-05 21:17:02 +00:00
/// Server
yubico_server : String , true , option ;
} ,
2019-04-11 16:40:03 +00:00
/// Global Duo settings (Note that users can override them)
2019-04-07 16:58:15 +00:00
duo : _enable_duo {
/// Enabled
2019-04-11 16:40:03 +00:00
_enable_duo : bool , true , def , false ;
2019-04-07 16:58:15 +00:00
/// Integration Key
duo_ikey : String , true , option ;
/// Secret Key
duo_skey : Pass , true , option ;
/// Host
duo_host : String , true , option ;
2019-04-11 14:08:26 +00:00
/// Application Key (generated automatically)
_duo_akey : Pass , false , option ;
2019-04-07 16:58:15 +00:00
} ,
2019-02-05 21:17:02 +00:00
/// SMTP Email Settings
2019-02-05 23:38:57 +00:00
smtp : _enable_smtp {
/// Enabled
_enable_smtp : bool , true , def , true ;
2019-02-05 21:17:02 +00:00
/// Host
smtp_host : String , true , option ;
/// Enable SSL
smtp_ssl : bool , true , def , true ;
2019-03-10 13:44:42 +00:00
/// Use explicit TLS |> Enabling this would force the use of an explicit TLS connection, instead of upgrading an insecure one with STARTTLS
smtp_explicit_tls : bool , true , def , false ;
2019-02-05 21:17:02 +00:00
/// Port
2019-03-10 13:44:42 +00:00
smtp_port : u16 , true , auto , | c | if c . smtp_explicit_tls { 465 } else if c . smtp_ssl { 587 } else { 25 } ;
2019-02-05 21:17:02 +00:00
/// From Address
smtp_from : String , true , def , String ::new ( ) ;
/// From Name
smtp_from_name : String , true , def , " Bitwarden_RS " . to_string ( ) ;
/// Username
smtp_username : String , true , option ;
/// Password
2019-02-08 19:49:04 +00:00
smtp_password : Pass , true , option ;
2019-08-23 23:22:14 +00:00
/// Json form auth mechanism |> Defaults for ssl is "Plain" and "Login" and nothing for non-ssl connections. Possible values: ["Plain", "Login", "Xoauth2"]
smtp_auth_mechanism : String , true , option ;
2019-11-06 20:39:33 +00:00
/// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
2020-07-04 23:59:15 +00:00
smtp_timeout : u64 , true , def , 15 ;
/// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters
helo_name : String , true , option ;
2019-02-05 21:17:02 +00:00
} ,
2019-10-16 05:10:27 +00:00
/// 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 ( ) ;
/// Token number length |> Length of the numbers in an email token. Minimum of 6. Maximum is 19.
email_token_size : u32 , 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.
email_expiration_time : u64 , true , def , 600 ;
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
email_attempts_limit : u64 , true , def , 3 ;
} ,
2019-02-02 00:09:21 +00:00
}
2019-01-25 17:23:51 +00:00
2019-02-02 15:47:27 +00:00
fn validate_config ( cfg : & ConfigItems ) -> Result < ( ) , Error > {
2019-10-08 17:34:47 +00:00
let db_url = cfg . database_url . to_lowercase ( ) ;
2020-07-04 20:13:27 +00:00
if cfg! ( feature = " sqlite " )
& & ( db_url . starts_with ( " mysql: " ) | | db_url . starts_with ( " postgresql: " ) | | db_url . starts_with ( " postgres: " ) )
{
2019-11-02 16:39:01 +00:00
err! ( " `DATABASE_URL` is meant for MySQL or Postgres, while this server is meant for SQLite " )
2019-10-08 17:34:47 +00:00
}
2019-11-02 16:39:01 +00:00
if cfg! ( feature = " mysql " ) & & ! db_url . starts_with ( " mysql: " ) {
err! ( " `DATABASE_URL` should start with mysql: when using the MySQL server " )
2019-10-08 17:34:47 +00:00
}
2020-07-04 20:13:27 +00:00
if cfg! ( feature = " postgresql " ) & & ! ( db_url . starts_with ( " postgresql: " ) | | db_url . starts_with ( " postgres: " ) ) {
2019-11-02 16:39:01 +00:00
err! ( " `DATABASE_URL` should start with postgresql: when using the PostgreSQL server " )
2019-10-08 17:34:47 +00:00
}
2020-03-09 21:04:03 +00:00
let dom = cfg . domain . to_lowercase ( ) ;
2020-02-23 13:55:27 +00:00
if ! dom . starts_with ( " http:// " ) & & ! dom . starts_with ( " https:// " ) {
err! ( " DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com' " ) ;
}
2019-10-08 17:34:47 +00:00
2020-04-09 08:42:27 +00:00
let whitelist = & cfg . signups_domains_whitelist ;
if ! whitelist . is_empty ( ) & & whitelist . split ( ',' ) . any ( | d | d . trim ( ) . is_empty ( ) ) {
err! ( " `SIGNUPS_DOMAINS_WHITELIST` contains empty tokens " ) ;
}
2019-03-07 19:21:50 +00:00
if let Some ( ref token ) = cfg . admin_token {
2020-01-30 21:10:50 +00:00
if token . trim ( ) . is_empty ( ) & & ! cfg . disable_admin_token {
2020-04-10 03:55:08 +00:00
println! ( " [WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled. " ) ;
println! ( " [WARNING] To enable the admin page without a token, use `DISABLE_ADMIN_TOKEN`. " ) ;
2019-03-07 19:21:50 +00:00
}
}
2019-08-31 15:47:52 +00:00
if cfg . _enable_duo
& & ( cfg . duo_host . is_some ( ) | | cfg . duo_ikey . is_some ( ) | | cfg . duo_skey . is_some ( ) )
2019-04-11 14:08:26 +00:00
& & ! ( cfg . duo_host . is_some ( ) & & cfg . duo_ikey . is_some ( ) & & cfg . duo_skey . is_some ( ) )
2019-04-07 16:58:15 +00:00
{
2019-04-11 14:08:26 +00:00
err! ( " All Duo options need to be set for global Duo support " )
2019-04-07 16:58:15 +00:00
}
2019-08-31 15:47:52 +00:00
if cfg . _enable_yubico & & cfg . yubico_client_id . is_some ( ) ! = cfg . yubico_secret_key . is_some ( ) {
2019-02-02 15:47:27 +00:00
err! ( " Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support " )
}
2019-08-31 15:47:52 +00:00
if cfg . _enable_smtp {
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 " )
}
2019-02-02 15:47:27 +00:00
2019-08-31 15:47:52 +00:00
if cfg . smtp_username . is_some ( ) ! = cfg . smtp_password . is_some ( ) {
err! ( " Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication " )
}
2019-02-02 15:47:27 +00:00
2019-08-31 15:47:52 +00:00
if cfg . _enable_email_2fa & & ( ! cfg . _enable_smtp | | cfg . smtp_host . is_none ( ) ) {
err! ( " To enable email 2FA, SMTP must be configured " )
}
if cfg . _enable_email_2fa & & cfg . email_token_size < 6 {
err! ( " `EMAIL_TOKEN_SIZE` has a minimum size of 6 " )
}
2019-08-26 18:26:54 +00:00
2019-08-31 15:47:52 +00:00
if cfg . _enable_email_2fa & & cfg . email_token_size > 19 {
err! ( " `EMAIL_TOKEN_SIZE` has a maximum size of 19 " )
}
2019-08-26 18:26:54 +00:00
}
2019-02-02 15:47:27 +00:00
Ok ( ( ) )
}
2020-02-19 05:27:00 +00:00
/// Extracts an RFC 6454 web origin from a URL.
fn extract_url_origin ( url : & str ) -> String {
2020-02-23 13:55:27 +00:00
match Url ::parse ( url ) {
Ok ( u ) = > u . origin ( ) . ascii_serialization ( ) ,
Err ( e ) = > {
println! ( " Error validating domain: {} " , e ) ;
String ::new ( )
}
}
2020-02-19 05:27:00 +00:00
}
/// Extracts the path from a URL.
/// All trailing '/' chars are trimmed, even if the path is a lone '/'.
fn extract_url_path ( url : & str ) -> String {
2020-02-23 13:55:27 +00:00
match Url ::parse ( url ) {
Ok ( u ) = > u . path ( ) . trim_end_matches ( '/' ) . to_string ( ) ,
Err ( _ ) = > {
// We already print it in the method above, no need to do it again
String ::new ( )
}
}
2020-02-19 05:27:00 +00:00
}
2019-02-02 00:09:21 +00:00
impl Config {
2019-02-05 23:38:57 +00:00
pub fn load ( ) -> Result < Self , Error > {
// Loading from env and file
let _env = ConfigBuilder ::from_env ( ) ;
let _usr = ConfigBuilder ::from_file ( & CONFIG_FILE ) . unwrap_or_default ( ) ;
// Create merged config, config file overwrites env
2019-04-11 14:08:26 +00:00
let builder = _env . merge ( & _usr , true ) ;
2019-02-05 23:38:57 +00:00
// Fill any missing with defaults
let config = builder . build ( ) ;
validate_config ( & config ) ? ;
Ok ( Config {
2019-12-27 17:37:14 +00:00
inner : RwLock ::new ( Inner { templates : load_templates ( & config . templates_folder ) , config , _env , _usr } ) ,
2019-02-05 23:38:57 +00:00
} )
}
2019-02-02 15:47:27 +00:00
pub fn update_config ( & self , other : ConfigBuilder ) -> Result < ( ) , Error > {
2019-02-02 23:22:18 +00:00
// Remove default values
2019-02-05 23:38:57 +00:00
//let builder = other.remove(&self.inner.read().unwrap()._env);
// TODO: Remove values that are defaults, above only checks those set by env and not the defaults
let builder = other ;
2019-02-02 15:47:27 +00:00
2019-02-02 23:22:18 +00:00
// Serialize now before we consume the builder
let config_str = serde_json ::to_string_pretty ( & builder ) ? ;
2019-02-02 16:45:25 +00:00
2019-02-02 23:22:18 +00:00
// Prepare the combined config
let config = {
let env = & self . inner . read ( ) . unwrap ( ) . _env ;
2019-04-11 14:08:26 +00:00
env . merge ( & builder , false ) . build ( )
2019-02-02 23:22:18 +00:00
} ;
validate_config ( & config ) ? ;
// Save both the user and the combined config
{
let mut writer = self . inner . write ( ) . unwrap ( ) ;
writer . config = config ;
writer . _usr = builder ;
}
2019-02-02 15:47:27 +00:00
2019-02-02 16:45:25 +00:00
//Save to file
use std ::{ fs ::File , io ::Write } ;
2019-02-04 00:37:25 +00:00
let mut file = File ::create ( & * CONFIG_FILE ) ? ;
2019-02-02 16:45:25 +00:00
file . write_all ( config_str . as_bytes ( ) ) ? ;
2019-02-02 15:47:27 +00:00
Ok ( ( ) )
}
2019-04-11 14:08:26 +00:00
pub fn update_config_partial ( & self , other : ConfigBuilder ) -> Result < ( ) , Error > {
let builder = {
let usr = & self . inner . read ( ) . unwrap ( ) . _usr ;
usr . merge ( & other , false )
} ;
self . update_config ( builder )
}
2020-05-24 21:00:26 +00:00
/// Tests whether an email's domain is allowed. A domain is allowed if it
/// is in signups_domains_whitelist, or if no whitelist is set (so there
/// are no domain restrictions in effect).
pub fn is_email_domain_allowed ( & self , email : & str ) -> bool {
2019-11-28 20:59:05 +00:00
let e : Vec < & str > = email . rsplitn ( 2 , '@' ) . collect ( ) ;
2019-11-16 22:01:45 +00:00
if e . len ( ) ! = 2 | | e [ 0 ] . is_empty ( ) | | e [ 1 ] . is_empty ( ) {
warn! ( " Failed to parse email address '{}' " , email ) ;
2019-12-27 17:37:14 +00:00
return false ;
2019-11-16 22:01:45 +00:00
}
2020-04-11 21:51:36 +00:00
let email_domain = e [ 0 ] . to_lowercase ( ) ;
2020-04-09 08:42:27 +00:00
let whitelist = self . signups_domains_whitelist ( ) ;
2020-03-09 21:04:03 +00:00
2020-05-24 21:00:26 +00:00
whitelist . is_empty ( ) | | whitelist . split ( ',' ) . any ( | d | d . trim ( ) = = email_domain )
2020-04-09 08:42:27 +00:00
}
/// Tests whether signup is allowed for an email address, taking into
/// account the signups_allowed and signups_domains_whitelist settings.
pub fn is_signup_allowed ( & self , email : & str ) -> bool {
if ! self . signups_domains_whitelist ( ) . is_empty ( ) {
// The whitelist setting overrides the signups_allowed setting.
2020-05-24 21:00:26 +00:00
self . is_email_domain_allowed ( email )
2020-04-09 08:42:27 +00:00
} else {
self . signups_allowed ( )
}
2019-11-16 22:01:45 +00:00
}
2019-02-06 16:32:13 +00:00
pub fn delete_user_config ( & self ) -> Result < ( ) , Error > {
crate ::util ::delete_file ( & CONFIG_FILE ) ? ;
// Empty user config
let usr = ConfigBuilder ::default ( ) ;
// Config now is env + defaults
let config = {
let env = & self . inner . read ( ) . unwrap ( ) . _env ;
env . build ( )
} ;
// Save configs
{
let mut writer = self . inner . write ( ) . unwrap ( ) ;
writer . config = config ;
writer . _usr = usr ;
}
Ok ( ( ) )
}
2019-02-02 23:22:18 +00:00
pub fn private_rsa_key ( & self ) -> String {
format! ( " {} .der " , CONFIG . rsa_key_filename ( ) )
}
pub fn private_rsa_key_pem ( & self ) -> String {
format! ( " {} .pem " , CONFIG . rsa_key_filename ( ) )
}
pub fn public_rsa_key ( & self ) -> String {
format! ( " {} .pub.der " , CONFIG . rsa_key_filename ( ) )
}
2019-02-02 00:09:21 +00:00
pub fn mail_enabled ( & self ) -> bool {
2019-02-05 23:38:57 +00:00
let inner = & self . inner . read ( ) . unwrap ( ) . config ;
2019-02-06 16:32:13 +00:00
inner . _enable_smtp & & inner . smtp_host . is_some ( )
2019-02-05 23:38:57 +00:00
}
2019-04-11 14:08:26 +00:00
pub fn get_duo_akey ( & self ) -> String {
if let Some ( akey ) = self . _duo_akey ( ) {
akey
} else {
let akey = crate ::crypto ::get_random_64 ( ) ;
let akey_s = data_encoding ::BASE64 . encode ( & akey ) ;
// Save the new value
let mut builder = ConfigBuilder ::default ( ) ;
builder . _duo_akey = Some ( akey_s . clone ( ) ) ;
self . update_config_partial ( builder ) . ok ( ) ;
akey_s
}
2019-02-02 00:09:21 +00:00
}
2019-01-25 17:23:51 +00:00
2020-04-10 03:55:08 +00:00
/// Tests whether the admin token is set to a non-empty value.
pub fn is_admin_token_set ( & self ) -> bool {
let token = self . admin_token ( ) ;
2020-05-03 15:24:51 +00:00
token . is_some ( ) & & ! token . unwrap ( ) . trim ( ) . is_empty ( )
2020-04-10 03:55:08 +00:00
}
2019-02-02 00:09:21 +00:00
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 " ) ;
2020-01-26 14:29:14 +00:00
let hb = load_templates ( CONFIG . templates_folder ( ) ) ;
2019-02-02 00:09:21 +00:00
hb . render ( name , data ) . map_err ( Into ::into )
} else {
let hb = & CONFIG . inner . read ( ) . unwrap ( ) . templates ;
hb . render ( name , data ) . map_err ( Into ::into )
}
}
2019-01-25 17:23:51 +00:00
}
2020-01-26 14:29:14 +00:00
use handlebars ::{ Context , Handlebars , Helper , HelperResult , Output , RenderContext , RenderError , Renderable } ;
2019-02-02 23:22:18 +00:00
2020-01-26 14:29:14 +00:00
fn load_templates < P > ( path : P ) -> Handlebars < 'static >
where
P : AsRef < std ::path ::Path > ,
{
2019-01-25 17:23:51 +00:00
let mut hb = Handlebars ::new ( ) ;
// Error on missing params
hb . set_strict_mode ( true ) ;
2019-02-17 14:22:27 +00:00
// Register helpers
2020-01-26 14:29:14 +00:00
hb . register_helper ( " case " , Box ::new ( case_helper ) ) ;
hb . register_helper ( " jsesc " , Box ::new ( js_escape_helper ) ) ;
2019-01-25 17:23:51 +00:00
macro_rules ! reg {
( $name :expr ) = > { {
let template = include_str! ( concat! ( " static/templates/ " , $name , " .hbs " ) ) ;
hb . register_template_string ( $name , template ) . unwrap ( ) ;
} } ;
2019-02-10 18:12:34 +00:00
( $name :expr , $ext :expr ) = > { {
reg! ( $name ) ;
reg! ( concat! ( $name , $ext ) ) ;
} } ;
2019-01-25 17:23:51 +00:00
}
// First register default templates here
2019-11-25 05:28:49 +00:00
reg! ( " email/change_email " , " .html " ) ;
reg! ( " email/delete_account " , " .html " ) ;
2019-02-10 18:12:34 +00:00
reg! ( " email/invite_accepted " , " .html " ) ;
reg! ( " email/invite_confirmed " , " .html " ) ;
2019-07-22 06:26:24 +00:00
reg! ( " email/new_device_logged_in " , " .html " ) ;
2019-02-10 18:12:34 +00:00
reg! ( " email/pw_hint_none " , " .html " ) ;
reg! ( " email/pw_hint_some " , " .html " ) ;
reg! ( " email/send_org_invite " , " .html " ) ;
2019-08-03 06:07:14 +00:00
reg! ( " email/twofactor_email " , " .html " ) ;
2019-11-25 05:28:49 +00:00
reg! ( " email/verify_email " , " .html " ) ;
reg! ( " email/welcome " , " .html " ) ;
reg! ( " email/welcome_must_verify " , " .html " ) ;
2020-02-26 10:02:22 +00:00
reg! ( " email/smtp_test " , " .html " ) ;
2019-01-25 17:23:51 +00:00
reg! ( " admin/base " ) ;
reg! ( " admin/login " ) ;
2020-05-28 08:42:36 +00:00
reg! ( " admin/settings " ) ;
reg! ( " admin/users " ) ;
reg! ( " admin/organizations " ) ;
reg! ( " admin/diagnostics " ) ;
2019-01-25 17:23:51 +00:00
// And then load user templates to overwrite the defaults
// Use .hbs extension for the files
// Templates get registered with their relative name
hb . register_templates_directory ( " .hbs " , path ) . unwrap ( ) ;
hb
}
2019-02-02 23:22:18 +00:00
2020-01-26 14:29:14 +00:00
fn case_helper < ' reg , ' rc > (
h : & Helper < ' reg , ' rc > ,
r : & ' reg Handlebars ,
ctx : & ' rc Context ,
rc : & mut RenderContext < ' reg , ' rc > ,
out : & mut dyn Output ,
) -> HelperResult {
let param = h
. param ( 0 )
. ok_or_else ( | | RenderError ::new ( " Param not found for helper \" case \" " ) ) ? ;
let value = param . value ( ) . clone ( ) ;
if h . params ( ) . iter ( ) . skip ( 1 ) . any ( | x | x . value ( ) = = & value ) {
h . template ( ) . map ( | t | t . render ( r , ctx , rc , out ) ) . unwrap_or ( Ok ( ( ) ) )
} else {
Ok ( ( ) )
2019-02-02 23:22:18 +00:00
}
}
2019-02-17 14:22:27 +00:00
2020-01-26 14:29:14 +00:00
fn js_escape_helper < ' reg , ' rc > (
h : & Helper < ' reg , ' rc > ,
_r : & ' reg Handlebars ,
_ctx : & ' rc Context ,
_rc : & mut RenderContext < ' reg , ' rc > ,
out : & mut dyn Output ,
) -> HelperResult {
let param = h
. param ( 0 )
. ok_or_else ( | | RenderError ::new ( " Param not found for helper \" js_escape \" " ) ) ? ;
let value = param
. value ( )
. as_str ( )
. ok_or_else ( | | RenderError ::new ( " Param for helper \" js_escape \" is not a String " ) ) ? ;
let escaped_value = value . replace ( '\\' , " " ) . replace ( '\'' , " \\ x22 " ) . replace ( '\"' , " \\ x27 " ) ;
let quoted_value = format! ( " " {} " " , escaped_value ) ;
out . write ( & quoted_value ) ? ;
Ok ( ( ) )
2019-02-17 14:22:27 +00:00
}