1 use std::process::exit;
2 use std::sync::RwLock;
3
4 use once_cell::sync::Lazy;
5 use reqwest::Url;
6
7 use crate::{
8 db::DbConnType,
9 error::Error,
10 util::{get_env, get_env_bool},
11 };
12
13 static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
14 let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
15 get_env("CONFIG_FILE").unwrap_or_else(|| format!("{}/config.json", data_folder))
16 });
17
18 pub static CONFIG: Lazy<Config> = Lazy::new(|| {
19 Config::load().unwrap_or_else(|e| {
20 println!("Error loading config:\n\t{:?}\n", e);
21 exit(12)
22 })
23 });
24
25 pub type Pass = String;
26
27 macro_rules! make_config {
28 ($(
29 $(#[doc = $groupdoc:literal])?
30 $group:ident $(: $group_enabled:ident)? {
31 $(
32 $(#[doc = $doc:literal])+
33 $name:ident : $ty:ident, $editable:literal, $none_action:ident $(, $default:expr)?;
34 )+},
35 )+) => {
36 pub struct Config { inner: RwLock<Inner> }
37
38 struct Inner {
39 templates: Handlebars<'static>,
40 config: ConfigItems,
41
42 _env: ConfigBuilder,
43 _usr: ConfigBuilder,
44
45 _overrides: Vec<String>,
46 }
47
48 #[derive(Clone, Default, Deserialize, Serialize)]
49 pub struct ConfigBuilder {
50 $($(
51 #[serde(skip_serializing_if = "Option::is_none")]
52 $name: Option<$ty>,
53 )+)+
54 }
55
56 impl ConfigBuilder {
57 #[allow(clippy::field_reassign_with_default)]
58 fn from_env() -> Self {
59 match dotenv::from_path(".env") {
60 Ok(_) => (),
61 Err(e) => match e {
62 dotenv::Error::LineParse(msg, pos) => {
63 panic!("Error loading the .env file:\nNear {:?} on position {}\nPlease fix and restart!\n", msg, pos);
64 },
65 dotenv::Error::Io(ioerr) => match ioerr.kind() {
66 std::io::ErrorKind::NotFound => {
67 println!("[INFO] No .env file found.\n");
68 },
69 std::io::ErrorKind::PermissionDenied => {
70 println!("[WARNING] Permission Denied while trying to read the .env file!\n");
71 },
72 _ => {
73 println!("[WARNING] Reading the .env file failed:\n{:?}\n", ioerr);
74 }
75 },
76 _ => {
77 println!("[WARNING] Reading the .env file failed:\n{:?}\n", e);
78 }
79 }
80 };
81
82 let mut builder = ConfigBuilder::default();
83 $($(
84 builder.$name = make_config! { @getenv &stringify!($name).to_uppercase(), $ty };
85 )+)+
86
87 builder
88 }
89
90 fn from_file(path: &str) -> Result<Self, Error> {
91 use crate::util::read_file_string;
92 let config_str = read_file_string(path)?;
93 serde_json::from_str(&config_str).map_err(Into::into)
94 }
95
96 /// Merges the values of both builders into a new builder.
97 /// If both have the same element, `other` wins.
98 fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec<String>) -> Self {
99 let mut builder = self.clone();
100 $($(
101 if let v @Some(_) = &other.$name {
102 builder.$name = v.clone();
103
104 if self.$name.is_some() {
105 overrides.push(stringify!($name).to_uppercase());
106 }
107 }
108 )+)+
109
110 if show_overrides && !overrides.is_empty() {
111 // We can't use warn! here because logging isn't setup yet.
112 println!("[WARNING] The following environment variables are being overriden by the config file,");
113 println!("[WARNING] please use the admin panel to make changes to them:");
114 println!("[WARNING] {}\n", overrides.join(", "));
115 }
116
117 builder
118 }
119
120 fn build(&self) -> ConfigItems {
121 let mut config = ConfigItems::default();
122 let _domain_set = self.domain.is_some();
123 $($(
124 config.$name = make_config!{ @build self.$name.clone(), &config, $none_action, $($default)? };
125 )+)+
126 config.domain_set = _domain_set;
127
128 config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase();
129 config.org_creation_users = config.org_creation_users.trim().to_lowercase();
130
131 config
132 }
133 }
134
135 #[derive(Clone, Default)]
136 struct ConfigItems { $($( $name: make_config!{@type $ty, $none_action}, )+)+ }
137
138 #[allow(unused)]
139 impl Config {
140 $($(
141 $(#[doc = $doc])+
142 pub fn $name(&self) -> make_config!{@type $ty, $none_action} {
143 self.inner.read().unwrap().config.$name.clone()
144 }
145 )+)+
146
147 pub fn prepare_json(&self) -> serde_json::Value {
148 let (def, cfg, overriden) = {
149 let inner = &self.inner.read().unwrap();
150 (inner._env.build(), inner.config.clone(), inner._overrides.clone())
151 };
152
153 fn _get_form_type(rust_type: &str) -> &'static str {
154 match rust_type {
155 "Pass" => "password",
156 "String" => "text",
157 "bool" => "checkbox",
158 _ => "number"
159 }
160 }
161
162 fn _get_doc(doc: &str) -> serde_json::Value {
163 let mut split = doc.split("|>").map(str::trim);
164
165 // We do not use the json!() macro here since that causes a lot of macro recursion.
166 // This slows down compile time and it also causes issues with rust-analyzer
167 serde_json::Value::Object({
168 let mut doc_json = serde_json::Map::new();
169 doc_json.insert("name".into(), serde_json::to_value(split.next()).unwrap());
170 doc_json.insert("description".into(), serde_json::to_value(split.next()).unwrap());
171 doc_json
172 })
173 }
174
175 // We do not use the json!() macro here since that causes a lot of macro recursion.
176 // This slows down compile time and it also causes issues with rust-analyzer
177 serde_json::Value::Array(<[_]>::into_vec(Box::new([
178 $(
179 serde_json::Value::Object({
180 let mut group = serde_json::Map::new();
181 group.insert("group".into(), (stringify!($group)).into());
182 group.insert("grouptoggle".into(), (stringify!($($group_enabled)?)).into());
183 group.insert("groupdoc".into(), (make_config!{ @show $($groupdoc)? }).into());
184
185 group.insert("elements".into(), serde_json::Value::Array(<[_]>::into_vec(Box::new([
186 $(
187 serde_json::Value::Object({
188 let mut element = serde_json::Map::new();
189 element.insert("editable".into(), ($editable).into());
190 element.insert("name".into(), (stringify!($name)).into());
191 element.insert("value".into(), serde_json::to_value(cfg.$name).unwrap());
192 element.insert("default".into(), serde_json::to_value(def.$name).unwrap());
193 element.insert("type".into(), (_get_form_type(stringify!($ty))).into());
194 element.insert("doc".into(), (_get_doc(concat!($($doc),+))).into());
195 element.insert("overridden".into(), (overriden.contains(&stringify!($name).to_uppercase())).into());
196 element
197 }),
198 )+
199 ]))));
200 group
201 }),
202 )+
203 ])))
204 }
205
206 pub fn get_support_json(&self) -> serde_json::Value {
207 // Define which config keys need to be masked.
208 // Pass types will always be masked and no need to put them in the list.
209 // Besides Pass, only String types will be masked via _privacy_mask.
210 const PRIVACY_CONFIG: &[&str] = &[
211 "allowed_iframe_ancestors",
212 "database_url",
213 "domain_origin",
214 "domain_path",
215 "domain",
216 "helo_name",
217 "org_creation_users",
218 "signups_domains_whitelist",
219 "smtp_from",
220 "smtp_host",
221 "smtp_username",
222 ];
223
224 let cfg = {
225 let inner = &self.inner.read().unwrap();
226 inner.config.clone()
227 };
228
229 /// We map over the string and remove all alphanumeric, _ and - characters.
230 /// This is the fastest way (within micro-seconds) instead of using a regex (which takes mili-seconds)
231 fn _privacy_mask(value: &str) -> String {
232 value.chars().map(|c|
233 match c {
234 c if c.is_alphanumeric() => '*',
235 '_' => '*',
236 '-' => '*',
237 _ => c
238 }
239 ).collect::<String>()
240 }
241
242 serde_json::Value::Object({
243 let mut json = serde_json::Map::new();
244 $($(
245 json.insert(stringify!($name).into(), make_config!{ @supportstr $name, cfg.$name, $ty, $none_action });
246 )+)+;
247 json
248 })
249 }
250
251 pub fn get_overrides(&self) -> Vec<String> {
252 let overrides = {
253 let inner = &self.inner.read().unwrap();
254 inner._overrides.clone()
255 };
256 overrides
257 }
258 }
259 };
260
261 // Support string print
262 ( @supportstr $name:ident, $value:expr, Pass, option ) => { serde_json::to_value($value.as_ref().map(|_| String::from("***"))).unwrap() }; // Optional pass, we map to an Option<String> with "***"
263 ( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { "***".into() }; // Required pass, we return "***"
264 ( @supportstr $name:ident, $value:expr, String, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config
265 if PRIVACY_CONFIG.contains(&stringify!($name)) {
266 serde_json::to_value($value.as_ref().map(|x| _privacy_mask(x) )).unwrap()
267 } else {
268 serde_json::to_value($value).unwrap()
269 }
270 };
271 ( @supportstr $name:ident, $value:expr, String, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config
272 if PRIVACY_CONFIG.contains(&stringify!($name)) {
273 _privacy_mask(&$value).into()
274 } else {
275 ($value).into()
276 }
277 };
278 ( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { serde_json::to_value($value).unwrap() }; // Optional other value, we return as is or convert to string to apply the privacy config
279 ( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { ($value).into() }; // Required other value, we return as is or convert to string to apply the privacy config
280
281 // Group or empty string
282 ( @show ) => { "" };
283 ( @show $lit:literal ) => { $lit };
284
285 // Wrap the optionals in an Option type
286 ( @type $ty:ty, option) => { Option<$ty> };
287 ( @type $ty:ty, $id:ident) => { $ty };
288
289 // Generate the values depending on none_action
290 ( @build $value:expr, $config:expr, option, ) => { $value };
291 ( @build $value:expr, $config:expr, def, $default:expr ) => { $value.unwrap_or($default) };
292 ( @build $value:expr, $config:expr, auto, $default_fn:expr ) => {{
293 match $value {
294 Some(v) => v,
295 None => {
296 let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
297 f($config)
298 }
299 }
300 }};
301 ( @build $value:expr, $config:expr, gen, $default_fn:expr ) => {{
302 let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
303 f($config)
304 }};
305
306 ( @getenv $name:expr, bool ) => { get_env_bool($name) };
307 ( @getenv $name:expr, $ty:ident ) => { get_env($name) };
308
309 }
310
311 //STRUCTURE:
312 // /// Short description (without this they won't appear on the list)
313 // group {
314 // /// Friendly Name |> Description (Optional)
315 // name: type, is_editable, action, <default_value (Optional)>
316 // }
317 //
318 // Where action applied when the value wasn't provided and can be:
319 // def: Use a default value
320 // auto: Value is auto generated based on other values
321 // option: Value is optional
322 // gen: Value is always autogenerated and it's original value ignored
323 make_config! {
324 folders {
325 /// Data folder |> Main data folder
326 data_folder: String, false, def, "data".to_string();
327 /// Database URL
328 database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
329 /// Icon cache folder
330 icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
331 /// Attachments folder
332 attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
333 /// Sends folder
334 sends_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "sends");
335 /// Templates folder
336 templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
337 /// Session JWT key
338 rsa_key_filename: String, false, auto, |c| format!("{}/{}", c.data_folder, "rsa_key");
339 /// Web vault folder
340 web_vault_folder: String, false, def, "web-vault/".to_string();
341 },
342 ws {
343 /// Enable websocket notifications
344 websocket_enabled: bool, false, def, false;
345 /// Websocket address
346 websocket_address: String, false, def, "0.0.0.0".to_string();
347 /// Websocket port
348 websocket_port: u16, false, def, 3012;
349 },
350 jobs {
351 /// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.
352 /// Set to 0 to globally disable scheduled jobs.
353 job_poll_interval_ms: u64, false, def, 30_000;
354 /// Send purge schedule |> Cron schedule of the job that checks for Sends past their deletion date.
355 /// Defaults to hourly. Set blank to disable this job.
356 send_purge_schedule: String, false, def, "0 5 * * * *".to_string();
357 /// Trash purge schedule |> Cron schedule of the job that checks for trashed items to delete permanently.
358 /// Defaults to daily. Set blank to disable this job.
359 trash_purge_schedule: String, false, def, "0 5 0 * * *".to_string();
360 /// Incomplete 2FA login schedule |> Cron schedule of the job that checks for incomplete 2FA logins.
361 /// Defaults to once every minute. Set blank to disable this job.
362 incomplete_2fa_schedule: String, false, def, "30 * * * * *".to_string();
363 /// Emergency notification reminder schedule |> Cron schedule of the job that sends expiration reminders to emergency access grantors.
364 /// Defaults to hourly. Set blank to disable this job.
365 emergency_notification_reminder_schedule: String, false, def, "0 5 * * * *".to_string();
366 /// Emergency request timeout schedule |> Cron schedule of the job that grants emergency access requests that have met the required wait time.
367 /// Defaults to hourly. Set blank to disable this job.
368 emergency_request_timeout_schedule: String, false, def, "0 5 * * * *".to_string();
369 },
370
371 /// General settings
372 settings {
373 /// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://'
374 /// and port, if it's different than the default. Some server functions don't work correctly without this value
375 domain: String, true, def, "http://localhost".to_string();
376 /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.
377 domain_set: bool, false, def, false;
378 /// Domain origin |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin)
379 domain_origin: String, false, auto, |c| extract_url_origin(&c.domain);
380 /// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path)
381 domain_path: String, false, auto, |c| extract_url_path(&c.domain);
382 /// Enable web vault
383 web_vault_enabled: bool, false, def, true;
384
385 /// Allow Sends |> Controls whether users are allowed to create Bitwarden Sends.
386 /// This setting applies globally to all users. To control this on a per-org basis instead, use the "Disable Send" org policy.
387 sends_allowed: bool, true, def, true;
388
389 /// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key
390 hibp_api_key: Pass, true, option;
391
392 /// Per-user attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per user. When this limit is reached, the user will not be allowed to upload further attachments.
393 user_attachment_limit: i64, true, option;
394 /// Per-organization attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per org. When this limit is reached, org members will not be allowed to upload further attachments for ciphers owned by that org.
395 org_attachment_limit: i64, true, option;
396
397 /// Trash auto-delete days |> Number of days to wait before auto-deleting a trashed item.
398 /// If unset, trashed items are not auto-deleted. This setting applies globally, so make
399 /// sure to inform all users of any changes to this setting.
400 trash_auto_delete_days: i64, true, option;
401
402 /// Incomplete 2FA time limit |> Number of minutes to wait before a 2FA-enabled login is
403 /// considered incomplete, resulting in an email notification. An incomplete 2FA login is one
404 /// where the correct master password was provided but the required 2FA step was not completed,
405 /// which potentially indicates a master password compromise. Set to 0 to disable this check.
406 /// This setting applies globally to all users.
407 incomplete_2fa_time_limit: i64, true, def, 3;
408
409 /// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
410 /// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
411 /// otherwise it will delete them and they won't be downloaded again.
412 disable_icon_download: bool, true, def, false;
413 /// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled
414 signups_allowed: bool, true, def, true;
415 /// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
416 signups_verify: bool, true, def, false;
417 /// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
418 signups_verify_resend_time: u64, true, def, 3_600;
419 /// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)
420 signups_verify_resend_limit: u32, true, def, 6;
421 /// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled
422 signups_domains_whitelist: String, true, def, "".to_string();
423 /// Org creation users |> Allow org creation only by this list of comma-separated user emails.
424 /// Blank or 'all' means all users can create orgs; 'none' means no users can create orgs.
425 org_creation_users: String, true, def, "".to_string();
426 /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are otherwise disabled
427 invitations_allowed: bool, true, def, true;
428 /// Allow emergency access |> Controls whether users can enable emergency access to their accounts. This setting applies globally to all users.
429 emergency_access_allowed: bool, true, def, true;
430 /// Password iterations |> Number of server-side passwords hashing iterations.
431 /// The changes only apply when a user changes their password. Not recommended to lower the value
432 password_iterations: i32, true, def, 100_000;
433 /// Show password hint |> Controls whether a password hint should be shown directly in the web page
434 /// if SMTP service is not configured. Not recommended for publicly-accessible instances as this
435 /// provides unauthenticated access to potentially sensitive data.
436 show_password_hint: bool, true, def, false;
437
438 /// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
439 admin_token: Pass, true, option;
440
441 /// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
442 invitation_org_name: String, true, def, "Vaultwarden".to_string();
443 },
444
445 /// Advanced settings
446 advanced {
447 /// Client IP header |> If not present, the remote IP is used.
448 /// Set to the string "none" (without quotes), to disable any headers and just use the remote IP
449 ip_header: String, true, def, "X-Real-IP".to_string();
450 /// Internal IP header property, used to avoid recomputing each time
451 _ip_header_enabled: bool, false, gen, |c| &c.ip_header.trim().to_lowercase() != "none";
452 /// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
453 icon_cache_ttl: u64, true, def, 2_592_000;
454 /// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
455 icon_cache_negttl: u64, true, def, 259_200;
456 /// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
457 icon_download_timeout: u64, true, def, 10;
458 /// Icon blacklist Regex |> Any domains or IPs that match this regex won't be fetched by the icon service.
459 /// Useful to hide other servers in the local network. Check the WIKI for more details
460 icon_blacklist_regex: String, true, option;
461 /// Icon blacklist non global IPs |> Any IP which is not defined as a global IP will be blacklisted.
462 /// Usefull to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
463 icon_blacklist_non_global_ips: bool, true, def, true;
464
465 /// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
466 /// Note that the checkbox would still be present, but ignored.
467 disable_2fa_remember: bool, true, def, false;
468
469 /// Disable authenticator time drifted codes to be valid |> Enabling this only allows the current TOTP code to be valid
470 /// TOTP codes of the previous and next 30 seconds will be invalid.
471 authenticator_disable_time_drift: bool, true, def, false;
472
473 /// Require new device emails |> When a user logs in an email is required to be sent.
474 /// If sending the email fails the login attempt will fail.
475 require_device_email: bool, true, def, false;
476
477 /// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
478 /// ONLY use this during development, as it can slow down the server
479 reload_templates: bool, true, def, false;
480 /// Enable extended logging
481 extended_logging: bool, false, def, true;
482 /// Log timestamp format
483 log_timestamp_format: String, true, def, "%Y-%m-%d %H:%M:%S.%3f".to_string();
484 /// Enable the log to output to Syslog
485 use_syslog: bool, false, def, false;
486 /// Log file path
487 log_file: String, false, option;
488 /// Log level
489 log_level: String, false, def, "Info".to_string();
490
491 /// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using vaultwarden on some exotic filesystems,
492 /// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
493 enable_db_wal: bool, false, def, true;
494
495 /// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely
496 db_connection_retries: u32, false, def, 15;
497
498 /// Database connection pool size
499 database_max_conns: u32, false, def, 10;
500
501 /// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
502 disable_admin_token: bool, true, def, false;
503
504 /// Allowed iframe ancestors (Know the risks!) |> Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets
505 allowed_iframe_ancestors: String, true, def, String::new();
506 },
507
508 /// Yubikey settings
509 yubico: _enable_yubico {
510 /// Enabled
511 _enable_yubico: bool, true, def, true;
512 /// Client ID
513 yubico_client_id: String, true, option;
514 /// Secret Key
515 yubico_secret_key: Pass, true, option;
516 /// Server
517 yubico_server: String, true, option;
518 },
519
520 /// Global Duo settings (Note that users can override them)
521 duo: _enable_duo {
522 /// Enabled
523 _enable_duo: bool, true, def, false;
524 /// Integration Key
525 duo_ikey: String, true, option;
526 /// Secret Key
527 duo_skey: Pass, true, option;
528 /// Host
529 duo_host: String, true, option;
530 /// Application Key (generated automatically)
531 _duo_akey: Pass, false, option;
532 },
533
534 /// SMTP Email Settings
535 smtp: _enable_smtp {
536 /// Enabled
537 _enable_smtp: bool, true, def, true;
538 /// Host
539 smtp_host: String, true, option;
540 /// Enable Secure SMTP |> (Explicit) - Enabling this by default would use STARTTLS (Standard ports 587 or 25)
541 smtp_ssl: bool, true, def, true;
542 /// Force TLS |> (Implicit) - Enabling this would force the use of an SSL/TLS connection, instead of upgrading an insecure one with STARTTLS (Standard port 465)
543 smtp_explicit_tls: bool, true, def, false;
544 /// Port
545 smtp_port: u16, true, auto, |c| if c.smtp_explicit_tls {465} else if c.smtp_ssl {587} else {25};
546 /// From Address
547 smtp_from: String, true, def, String::new();
548 /// From Name
549 smtp_from_name: String, true, def, "Vaultwarden".to_string();
550 /// Username
551 smtp_username: String, true, option;
552 /// Password
553 smtp_password: Pass, true, option;
554 /// SMTP Auth mechanism |> Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. Possible values: ["Plain", "Login", "Xoauth2"]. Multiple options need to be separated by a comma ','.
555 smtp_auth_mechanism: String, true, option;
556 /// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
557 smtp_timeout: u64, true, def, 15;
558 /// 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
559 helo_name: String, true, option;
560 /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
561 smtp_debug: bool, false, def, false;
562 /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks!
563 smtp_accept_invalid_certs: bool, true, def, false;
564 /// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
565 smtp_accept_invalid_hostnames: bool, true, def, false;
566 },
567
568 /// Email 2FA Settings
569 email_2fa: _enable_email_2fa {
570 /// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
571 _enable_email_2fa: bool, true, auto, |c| c._enable_smtp && c.smtp_host.is_some();
572 /// Email token size |> Number of digits in an email token (min: 6, max: 19). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting.
573 email_token_size: u32, true, def, 6;
574 /// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
575 email_expiration_time: u64, true, def, 600;
576 /// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
577 email_attempts_limit: u64, true, def, 3;
578 },
579 }
580
validate_config(cfg: &ConfigItems) -> Result<(), Error>581 fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
582 // Validate connection URL is valid and DB feature is enabled
583 DbConnType::from_url(&cfg.database_url)?;
584
585 let limit = 256;
586 if cfg.database_max_conns < 1 || cfg.database_max_conns > limit {
587 err!(format!("`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {}.", limit,));
588 }
589
590 let dom = cfg.domain.to_lowercase();
591 if !dom.starts_with("http://") && !dom.starts_with("https://") {
592 err!(
593 "DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'"
594 );
595 }
596
597 let whitelist = &cfg.signups_domains_whitelist;
598 if !whitelist.is_empty() && whitelist.split(',').any(|d| d.trim().is_empty()) {
599 err!("`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens");
600 }
601
602 let org_creation_users = cfg.org_creation_users.trim().to_lowercase();
603 if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none")
604 && org_creation_users.split(',').any(|u| !u.contains('@'))
605 {
606 err!("`ORG_CREATION_USERS` contains invalid email addresses");
607 }
608
609 if let Some(ref token) = cfg.admin_token {
610 if token.trim().is_empty() && !cfg.disable_admin_token {
611 println!("[WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled.");
612 println!("[WARNING] To enable the admin page without a token, use `DISABLE_ADMIN_TOKEN`.");
613 }
614 }
615
616 if cfg._enable_duo
617 && (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some())
618 && !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some())
619 {
620 err!("All Duo options need to be set for global Duo support")
621 }
622
623 if cfg._enable_yubico && cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
624 err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support")
625 }
626
627 if cfg._enable_smtp {
628 if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() {
629 err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support")
630 }
631
632 if cfg.smtp_host.is_some() && !cfg.smtp_from.contains('@') {
633 err!("SMTP_FROM does not contain a mandatory @ sign")
634 }
635
636 if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() {
637 err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication")
638 }
639
640 if cfg._enable_email_2fa && (!cfg._enable_smtp || cfg.smtp_host.is_none()) {
641 err!("To enable email 2FA, SMTP must be configured")
642 }
643
644 if cfg._enable_email_2fa && cfg.email_token_size < 6 {
645 err!("`EMAIL_TOKEN_SIZE` has a minimum size of 6")
646 }
647
648 if cfg._enable_email_2fa && cfg.email_token_size > 19 {
649 err!("`EMAIL_TOKEN_SIZE` has a maximum size of 19")
650 }
651 }
652
653 // Check if the icon blacklist regex is valid
654 if let Some(ref r) = cfg.icon_blacklist_regex {
655 let validate_regex = regex::Regex::new(r);
656 match validate_regex {
657 Ok(_) => (),
658 Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {:#?}", e)),
659 }
660 }
661
662 Ok(())
663 }
664
665 /// Extracts an RFC 6454 web origin from a URL.
extract_url_origin(url: &str) -> String666 fn extract_url_origin(url: &str) -> String {
667 match Url::parse(url) {
668 Ok(u) => u.origin().ascii_serialization(),
669 Err(e) => {
670 println!("Error validating domain: {}", e);
671 String::new()
672 }
673 }
674 }
675
676 /// Extracts the path from a URL.
677 /// All trailing '/' chars are trimmed, even if the path is a lone '/'.
extract_url_path(url: &str) -> String678 fn extract_url_path(url: &str) -> String {
679 match Url::parse(url) {
680 Ok(u) => u.path().trim_end_matches('/').to_string(),
681 Err(_) => {
682 // We already print it in the method above, no need to do it again
683 String::new()
684 }
685 }
686 }
687
688 impl Config {
load() -> Result<Self, Error>689 pub fn load() -> Result<Self, Error> {
690 // Loading from env and file
691 let _env = ConfigBuilder::from_env();
692 let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
693
694 // Create merged config, config file overwrites env
695 let mut _overrides = Vec::new();
696 let builder = _env.merge(&_usr, true, &mut _overrides);
697
698 // Fill any missing with defaults
699 let config = builder.build();
700 validate_config(&config)?;
701
702 Ok(Config {
703 inner: RwLock::new(Inner {
704 templates: load_templates(&config.templates_folder),
705 config,
706 _env,
707 _usr,
708 _overrides,
709 }),
710 })
711 }
712
update_config(&self, other: ConfigBuilder) -> Result<(), Error>713 pub fn update_config(&self, other: ConfigBuilder) -> Result<(), Error> {
714 // Remove default values
715 //let builder = other.remove(&self.inner.read().unwrap()._env);
716
717 // TODO: Remove values that are defaults, above only checks those set by env and not the defaults
718 let builder = other;
719
720 // Serialize now before we consume the builder
721 let config_str = serde_json::to_string_pretty(&builder)?;
722
723 // Prepare the combined config
724 let mut overrides = Vec::new();
725 let config = {
726 let env = &self.inner.read().unwrap()._env;
727 env.merge(&builder, false, &mut overrides).build()
728 };
729 validate_config(&config)?;
730
731 // Save both the user and the combined config
732 {
733 let mut writer = self.inner.write().unwrap();
734 writer.config = config;
735 writer._usr = builder;
736 writer._overrides = overrides;
737 }
738
739 //Save to file
740 use std::{fs::File, io::Write};
741 let mut file = File::create(&*CONFIG_FILE)?;
742 file.write_all(config_str.as_bytes())?;
743
744 Ok(())
745 }
746
update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error>747 fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
748 let builder = {
749 let usr = &self.inner.read().unwrap()._usr;
750 let mut _overrides = Vec::new();
751 usr.merge(&other, false, &mut _overrides)
752 };
753 self.update_config(builder)
754 }
755
756 /// Tests whether an email's domain is allowed. A domain is allowed if it
757 /// is in signups_domains_whitelist, or if no whitelist is set (so there
758 /// are no domain restrictions in effect).
is_email_domain_allowed(&self, email: &str) -> bool759 pub fn is_email_domain_allowed(&self, email: &str) -> bool {
760 let e: Vec<&str> = email.rsplitn(2, '@').collect();
761 if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {
762 warn!("Failed to parse email address '{}'", email);
763 return false;
764 }
765 let email_domain = e[0].to_lowercase();
766 let whitelist = self.signups_domains_whitelist();
767
768 whitelist.is_empty() || whitelist.split(',').any(|d| d.trim() == email_domain)
769 }
770
771 /// Tests whether signup is allowed for an email address, taking into
772 /// account the signups_allowed and signups_domains_whitelist settings.
is_signup_allowed(&self, email: &str) -> bool773 pub fn is_signup_allowed(&self, email: &str) -> bool {
774 if !self.signups_domains_whitelist().is_empty() {
775 // The whitelist setting overrides the signups_allowed setting.
776 self.is_email_domain_allowed(email)
777 } else {
778 self.signups_allowed()
779 }
780 }
781
782 /// Tests whether the specified user is allowed to create an organization.
is_org_creation_allowed(&self, email: &str) -> bool783 pub fn is_org_creation_allowed(&self, email: &str) -> bool {
784 let users = self.org_creation_users();
785 if users.is_empty() || users == "all" {
786 true
787 } else if users == "none" {
788 false
789 } else {
790 let email = email.to_lowercase();
791 users.split(',').any(|u| u.trim() == email)
792 }
793 }
794
delete_user_config(&self) -> Result<(), Error>795 pub fn delete_user_config(&self) -> Result<(), Error> {
796 crate::util::delete_file(&CONFIG_FILE)?;
797
798 // Empty user config
799 let usr = ConfigBuilder::default();
800
801 // Config now is env + defaults
802 let config = {
803 let env = &self.inner.read().unwrap()._env;
804 env.build()
805 };
806
807 // Save configs
808 {
809 let mut writer = self.inner.write().unwrap();
810 writer.config = config;
811 writer._usr = usr;
812 writer._overrides = Vec::new();
813 }
814
815 Ok(())
816 }
817
private_rsa_key(&self) -> String818 pub fn private_rsa_key(&self) -> String {
819 format!("{}.pem", CONFIG.rsa_key_filename())
820 }
public_rsa_key(&self) -> String821 pub fn public_rsa_key(&self) -> String {
822 format!("{}.pub.pem", CONFIG.rsa_key_filename())
823 }
mail_enabled(&self) -> bool824 pub fn mail_enabled(&self) -> bool {
825 let inner = &self.inner.read().unwrap().config;
826 inner._enable_smtp && inner.smtp_host.is_some()
827 }
828
get_duo_akey(&self) -> String829 pub fn get_duo_akey(&self) -> String {
830 if let Some(akey) = self._duo_akey() {
831 akey
832 } else {
833 let akey = crate::crypto::get_random_64();
834 let akey_s = data_encoding::BASE64.encode(&akey);
835
836 // Save the new value
837 let builder = ConfigBuilder {
838 _duo_akey: Some(akey_s.clone()),
839 ..Default::default()
840 };
841 self.update_config_partial(builder).ok();
842
843 akey_s
844 }
845 }
846
847 /// Tests whether the admin token is set to a non-empty value.
is_admin_token_set(&self) -> bool848 pub fn is_admin_token_set(&self) -> bool {
849 let token = self.admin_token();
850
851 token.is_some() && !token.unwrap().trim().is_empty()
852 }
853
render_template<T: serde::ser::Serialize>( &self, name: &str, data: &T, ) -> Result<String, crate::error::Error>854 pub fn render_template<T: serde::ser::Serialize>(
855 &self,
856 name: &str,
857 data: &T,
858 ) -> Result<String, crate::error::Error> {
859 if CONFIG.reload_templates() {
860 warn!("RELOADING TEMPLATES");
861 let hb = load_templates(CONFIG.templates_folder());
862 hb.render(name, data).map_err(Into::into)
863 } else {
864 let hb = &CONFIG.inner.read().unwrap().templates;
865 hb.render(name, data).map_err(Into::into)
866 }
867 }
868 }
869
870 use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, Renderable};
871
load_templates<P>(path: P) -> Handlebars<'static> where P: AsRef<std::path::Path>,872 fn load_templates<P>(path: P) -> Handlebars<'static>
873 where
874 P: AsRef<std::path::Path>,
875 {
876 let mut hb = Handlebars::new();
877 // Error on missing params
878 hb.set_strict_mode(true);
879 // Register helpers
880 hb.register_helper("case", Box::new(case_helper));
881 hb.register_helper("jsesc", Box::new(js_escape_helper));
882
883 macro_rules! reg {
884 ($name:expr) => {{
885 let template = include_str!(concat!("static/templates/", $name, ".hbs"));
886 hb.register_template_string($name, template).unwrap();
887 }};
888 ($name:expr, $ext:expr) => {{
889 reg!($name);
890 reg!(concat!($name, $ext));
891 }};
892 }
893
894 // First register default templates here
895 reg!("email/email_header");
896 reg!("email/email_footer");
897 reg!("email/email_footer_text");
898
899 reg!("email/change_email", ".html");
900 reg!("email/delete_account", ".html");
901 reg!("email/emergency_access_invite_accepted", ".html");
902 reg!("email/emergency_access_invite_confirmed", ".html");
903 reg!("email/emergency_access_recovery_approved", ".html");
904 reg!("email/emergency_access_recovery_initiated", ".html");
905 reg!("email/emergency_access_recovery_rejected", ".html");
906 reg!("email/emergency_access_recovery_reminder", ".html");
907 reg!("email/emergency_access_recovery_timed_out", ".html");
908 reg!("email/incomplete_2fa_login", ".html");
909 reg!("email/invite_accepted", ".html");
910 reg!("email/invite_confirmed", ".html");
911 reg!("email/new_device_logged_in", ".html");
912 reg!("email/pw_hint_none", ".html");
913 reg!("email/pw_hint_some", ".html");
914 reg!("email/send_2fa_removed_from_org", ".html");
915 reg!("email/send_single_org_removed_from_org", ".html");
916 reg!("email/send_org_invite", ".html");
917 reg!("email/send_emergency_access_invite", ".html");
918 reg!("email/twofactor_email", ".html");
919 reg!("email/verify_email", ".html");
920 reg!("email/welcome", ".html");
921 reg!("email/welcome_must_verify", ".html");
922 reg!("email/smtp_test", ".html");
923
924 reg!("admin/base");
925 reg!("admin/login");
926 reg!("admin/settings");
927 reg!("admin/users");
928 reg!("admin/organizations");
929 reg!("admin/diagnostics");
930
931 // And then load user templates to overwrite the defaults
932 // Use .hbs extension for the files
933 // Templates get registered with their relative name
934 hb.register_templates_directory(".hbs", path).unwrap();
935
936 hb
937 }
938
case_helper<'reg, 'rc>( h: &Helper<'reg, 'rc>, r: &'reg Handlebars, ctx: &'rc Context, rc: &mut RenderContext<'reg, 'rc>, out: &mut dyn Output, ) -> HelperResult939 fn case_helper<'reg, 'rc>(
940 h: &Helper<'reg, 'rc>,
941 r: &'reg Handlebars,
942 ctx: &'rc Context,
943 rc: &mut RenderContext<'reg, 'rc>,
944 out: &mut dyn Output,
945 ) -> HelperResult {
946 let param = h.param(0).ok_or_else(|| RenderError::new("Param not found for helper \"case\""))?;
947 let value = param.value().clone();
948
949 if h.params().iter().skip(1).any(|x| x.value() == &value) {
950 h.template().map(|t| t.render(r, ctx, rc, out)).unwrap_or(Ok(()))
951 } else {
952 Ok(())
953 }
954 }
955
js_escape_helper<'reg, 'rc>( h: &Helper<'reg, 'rc>, _r: &'reg Handlebars, _ctx: &'rc Context, _rc: &mut RenderContext<'reg, 'rc>, out: &mut dyn Output, ) -> HelperResult956 fn js_escape_helper<'reg, 'rc>(
957 h: &Helper<'reg, 'rc>,
958 _r: &'reg Handlebars,
959 _ctx: &'rc Context,
960 _rc: &mut RenderContext<'reg, 'rc>,
961 out: &mut dyn Output,
962 ) -> HelperResult {
963 let param = h.param(0).ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?;
964
965 let no_quote = h.param(1).is_some();
966
967 let value =
968 param.value().as_str().ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?;
969
970 let mut escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
971 if !no_quote {
972 escaped_value = format!(""{}"", escaped_value);
973 }
974
975 out.write(&escaped_value)?;
976 Ok(())
977 }
978