config.rs 59 KB

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