mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 18:28:20 +02:00 
			
		
		
		
	Implement basic config loading and updating. No save to file yet.
This commit is contained in:
		| @@ -4,9 +4,11 @@ use rocket::http::{Cookie, Cookies, SameSite}; | ||||
| use rocket::request::{self, FlashMessage, Form, FromRequest, Request}; | ||||
| use rocket::response::{content::Html, Flash, Redirect}; | ||||
| use rocket::{Outcome, Route}; | ||||
| use rocket_contrib::json::Json; | ||||
|  | ||||
| use crate::api::{ApiResult, EmptyResult, JsonUpcase}; | ||||
| use crate::api::{ApiResult, EmptyResult}; | ||||
| use crate::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}; | ||||
| use crate::config::ConfigBuilder; | ||||
| use crate::db::{models::*, DbConn}; | ||||
| use crate::error::Error; | ||||
| use crate::mail; | ||||
| @@ -24,7 +26,6 @@ pub fn routes() -> Vec<Route> { | ||||
|         invite_user, | ||||
|         delete_user, | ||||
|         deauth_user, | ||||
|         get_config, | ||||
|         post_config, | ||||
|     ] | ||||
| } | ||||
| @@ -32,42 +33,16 @@ pub fn routes() -> Vec<Route> { | ||||
| const COOKIE_NAME: &str = "BWRS_ADMIN"; | ||||
| const ADMIN_PATH: &str = "/admin"; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct AdminTemplateData { | ||||
|     users: Vec<Value>, | ||||
|     page_content: String, | ||||
|     error: Option<String>, | ||||
| } | ||||
|  | ||||
| impl AdminTemplateData { | ||||
|     fn login(error: Option<String>) -> Self { | ||||
|         Self { | ||||
|             users: Vec::new(), | ||||
|             page_content: String::from("admin/login"), | ||||
|             error, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn admin(users: Vec<Value>) -> Self { | ||||
|         Self { | ||||
|             users, | ||||
|             page_content: String::from("admin/page"), | ||||
|             error: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn render(self) -> Result<String, Error> { | ||||
|         CONFIG.render_template("admin/base", &self) | ||||
|     } | ||||
| } | ||||
| const BASE_TEMPLATE: &str = "admin/base"; | ||||
|  | ||||
| #[get("/", rank = 2)] | ||||
| fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> { | ||||
|     // If there is an error, show it | ||||
|     let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg())); | ||||
|     let json = json!({"page_content": "admin/login", "error": msg}); | ||||
|  | ||||
|     // Return the page | ||||
|     let text = AdminTemplateData::login(msg).render()?; | ||||
|     let text = CONFIG.render_template(BASE_TEMPLATE, &json)?; | ||||
|     Ok(Html(text)) | ||||
| } | ||||
|  | ||||
| @@ -111,26 +86,47 @@ fn _validate_token(token: &str) -> bool { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct AdminTemplateData { | ||||
|     users: Vec<Value>, | ||||
|     page_content: String, | ||||
|     config: String, | ||||
| } | ||||
|  | ||||
| impl AdminTemplateData { | ||||
|     fn new(users: Vec<Value>) -> Self { | ||||
|         Self { | ||||
|             users, | ||||
|             page_content: String::from("admin/page"), | ||||
|             config: serde_json::to_string_pretty(&CONFIG.get_config()).unwrap(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn render(self) -> Result<String, Error> { | ||||
|         CONFIG.render_template(BASE_TEMPLATE, &self) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/", rank = 1)] | ||||
| fn admin_page(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { | ||||
|     let users = User::get_all(&conn); | ||||
|     let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect(); | ||||
|  | ||||
|     let text = AdminTemplateData::admin(users_json).render()?; | ||||
|     let text = AdminTemplateData::new(users_json).render()?; | ||||
|     Ok(Html(text)) | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| struct InviteData { | ||||
|     Email: String, | ||||
|     email: String, | ||||
| } | ||||
|  | ||||
| #[post("/invite", data = "<data>")] | ||||
| fn invite_user(data: JsonUpcase<InviteData>, _token: AdminToken, conn: DbConn) -> EmptyResult { | ||||
|     let data: InviteData = data.into_inner().data; | ||||
|     let email = data.Email.clone(); | ||||
|     if User::find_by_mail(&data.Email, &conn).is_some() { | ||||
| fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> EmptyResult { | ||||
|     let data: InviteData = data.into_inner(); | ||||
|     let email = data.email.clone(); | ||||
|     if User::find_by_mail(&data.email, &conn).is_some() { | ||||
|         err!("User already exists") | ||||
|     } | ||||
|  | ||||
| @@ -144,7 +140,7 @@ fn invite_user(data: JsonUpcase<InviteData>, _token: AdminToken, conn: DbConn) - | ||||
|         let org_name = "bitwarden_rs"; | ||||
|         mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None) | ||||
|     } else { | ||||
|         let mut invitation = Invitation::new(data.Email); | ||||
|         let mut invitation = Invitation::new(data.email); | ||||
|         invitation.save(&conn) | ||||
|     } | ||||
| } | ||||
| @@ -171,18 +167,13 @@ fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult { | ||||
|     user.save(&conn) | ||||
| } | ||||
|  | ||||
| #[get("/config")] | ||||
| fn get_config(_token: AdminToken) -> EmptyResult { | ||||
|     unimplemented!("Get config") | ||||
| } | ||||
|  | ||||
| #[post("/config", data = "<data>")] | ||||
| fn post_config(data: JsonUpcase<Value>, _token: AdminToken) -> EmptyResult { | ||||
|     let data: Value = data.into_inner().data; | ||||
| fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult { | ||||
|     let data: ConfigBuilder = data.into_inner(); | ||||
|  | ||||
|     info!("CONFIG: {:#?}", data); | ||||
|  | ||||
|     unimplemented!("Update config") | ||||
|     CONFIG.update_config(data) | ||||
| } | ||||
|  | ||||
| pub struct AdminToken {} | ||||
|   | ||||
| @@ -331,7 +331,7 @@ fn _header_map() -> HeaderMap { | ||||
|     use reqwest::header::*; | ||||
|  | ||||
|     macro_rules! headers { | ||||
|         ($( $name:ident : $value:literal),+ $(,)* ) => { | ||||
|         ($( $name:ident : $value:literal),+ $(,)? ) => { | ||||
|             let mut headers = HeaderMap::new(); | ||||
|             $( headers.insert($name, HeaderValue::from_static($value)); )+ | ||||
|             headers | ||||
|   | ||||
							
								
								
									
										125
									
								
								src/config.rs
									
									
									
									
									
								
							
							
						
						
									
										125
									
								
								src/config.rs
									
									
									
									
									
								
							| @@ -4,7 +4,6 @@ use std::sync::RwLock; | ||||
| use handlebars::Handlebars; | ||||
|  | ||||
| use crate::error::Error; | ||||
| use crate::util::IntoResult; | ||||
|  | ||||
| lazy_static! { | ||||
|     pub static ref CONFIG: Config = Config::load().unwrap_or_else(|e| { | ||||
| @@ -14,17 +13,62 @@ lazy_static! { | ||||
| } | ||||
|  | ||||
| macro_rules! make_config { | ||||
|     ( $( $name:ident : $ty:ty $(, $default_fn:expr)? );+ $(;)* ) => { | ||||
|     ( $( $name:ident : $ty:ty $(, $default_fn:expr)? );+ $(;)? ) => { | ||||
|  | ||||
|         pub struct Config { inner: RwLock<Inner> } | ||||
|  | ||||
|         struct Inner { | ||||
|             templates: Handlebars, | ||||
|             config: _Config, | ||||
|             config: ConfigItems, | ||||
|         } | ||||
|  | ||||
|         #[derive(Debug, Default, Serialize, Deserialize)] | ||||
|         struct _Config { $(pub $name: $ty),+ } | ||||
|         #[derive(Debug, Default, Deserialize)] | ||||
|         pub struct ConfigBuilder { | ||||
|             $($name: Option<$ty>),+ | ||||
|         } | ||||
|  | ||||
|         impl ConfigBuilder { | ||||
|             fn from_env() -> Self { | ||||
|                 dotenv::dotenv().ok(); | ||||
|                 use crate::util::get_env; | ||||
|  | ||||
|                 let mut builder = ConfigBuilder::default(); | ||||
|                 $( | ||||
|                     let $name = stringify!($name).to_uppercase(); | ||||
|                     builder.$name = make_config!{ @env &$name, $($default_fn)? }; | ||||
|                 )+ | ||||
|  | ||||
|                 builder | ||||
|             } | ||||
|  | ||||
|             fn from_file(path: &str) -> Result<Self, Error> { | ||||
|                 use crate::util::read_file_string; | ||||
|                 let config_str = read_file_string(path)?; | ||||
|                 serde_json::from_str(&config_str).map_err(Into::into) | ||||
|             } | ||||
|  | ||||
|             fn merge(&mut self, other: Self) { | ||||
|                 $( | ||||
|                     if let v @Some(_) = other.$name { | ||||
|                         self.$name = v; | ||||
|                     } | ||||
|                 )+ | ||||
|             } | ||||
|  | ||||
|             fn build(self) -> ConfigItems { | ||||
|                 let mut config = ConfigItems::default(); | ||||
|                 let _domain_set = self.domain.is_some(); | ||||
|                 $( | ||||
|                     config.$name = make_config!{ @build self.$name, &config, $($default_fn)? }; | ||||
|                 )+ | ||||
|                 config.domain_set = _domain_set; | ||||
|  | ||||
|                 config | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         #[derive(Debug, Clone, Default, Serialize)] | ||||
|         pub struct ConfigItems { $(pub $name: $ty),+ } | ||||
|  | ||||
|         paste::item! { | ||||
|         #[allow(unused)] | ||||
| @@ -39,14 +83,19 @@ macro_rules! make_config { | ||||
|             )+ | ||||
|  | ||||
|             pub fn load() -> Result<Self, Error> { | ||||
|                 use crate::util::get_env; | ||||
|                 dotenv::dotenv().ok(); | ||||
|                 // TODO: Get config.json from CONFIG_PATH env var or -c <CONFIG> console option | ||||
|  | ||||
|                 let mut config = _Config::default(); | ||||
|                 // Loading from file | ||||
|                 let mut builder = match ConfigBuilder::from_file("data/config.json") { | ||||
|                     Ok(builder) => builder, | ||||
|                     Err(_) => ConfigBuilder::default() | ||||
|                 }; | ||||
|  | ||||
|                 $( | ||||
|                     config.$name = make_config!{ @expr &stringify!($name).to_uppercase(), $ty, &config, $($default_fn)? }; | ||||
|                 )+ | ||||
|                 // Env variables overwrite config file | ||||
|                 builder.merge(ConfigBuilder::from_env()); | ||||
|  | ||||
|                 let config = builder.build(); | ||||
|                 validate_config(&config)?; | ||||
|  | ||||
|                 Ok(Config { | ||||
|                     inner: RwLock::new(Inner { | ||||
| @@ -60,19 +109,26 @@ macro_rules! make_config { | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     ( @expr $name:expr, $ty:ty, $config:expr, $default_fn:expr ) => {{ | ||||
|     ( @env $name:expr, $default_fn:expr ) => { get_env($name) }; | ||||
|  | ||||
|     ( @env $name:expr, ) => { | ||||
|         match get_env($name) { | ||||
|             v @ Some(_) => Some(v), | ||||
|             None => None | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     ( @build $value:expr,$config:expr, $default_fn:expr ) => { | ||||
|         match $value { | ||||
|             Some(v) => v, | ||||
|             None => { | ||||
|                 let f: &Fn(&_Config) -> _ = &$default_fn; | ||||
|                 f($config).into_result()? | ||||
|                 let f: &Fn(&ConfigItems) -> _ = &$default_fn; | ||||
|                 f($config) | ||||
|             } | ||||
|         } | ||||
|     }}; | ||||
|  | ||||
|     ( @expr $name:expr, $ty:ty, $config:expr, ) => { | ||||
|         get_env($name) | ||||
|     }; | ||||
|  | ||||
|     ( @build $value:expr, $config:expr, ) => { $value.unwrap_or(None) }; | ||||
| } | ||||
|  | ||||
| make_config! { | ||||
| @@ -121,13 +177,44 @@ make_config! { | ||||
|     smtp_host:              Option<String>; | ||||
|     smtp_ssl:               bool,   |_| true; | ||||
|     smtp_port:              u16,    |c| if c.smtp_ssl {587} else {25}; | ||||
|     smtp_from:              String, |c| if c.smtp_host.is_some() { err!("Please specify SMTP_FROM to enable SMTP support") } else { Ok(String::new() )}; | ||||
|     smtp_from:              String, |_| String::new(); | ||||
|     smtp_from_name:         String, |_| "Bitwarden_RS".to_string(); | ||||
|     smtp_username:          Option<String>; | ||||
|     smtp_password:          Option<String>; | ||||
| } | ||||
|  | ||||
| fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { | ||||
|     if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() { | ||||
|         err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support") | ||||
|     } | ||||
|  | ||||
|     if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() { | ||||
|         err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support") | ||||
|     } | ||||
|  | ||||
|     if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() { | ||||
|         err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication") | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| impl Config { | ||||
|     pub fn get_config(&self) -> ConfigItems { | ||||
|         self.inner.read().unwrap().config.clone() | ||||
|     } | ||||
|      | ||||
|     pub fn update_config(&self, other: ConfigBuilder) -> Result<(), Error> { | ||||
|         let config = other.build(); | ||||
|         validate_config(&config)?; | ||||
|  | ||||
|         self.inner.write().unwrap().config = config; | ||||
|  | ||||
|         // TODO: Save to file | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn mail_enabled(&self) -> bool { | ||||
|         self.inner.read().unwrap().config.smtp_host.is_some() | ||||
|     } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| use std::error::Error as StdError; | ||||
|  | ||||
| macro_rules! make_error { | ||||
|     ( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)* ) => { | ||||
|     ( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => { | ||||
|         #[derive(Display)] | ||||
|         enum ErrorKind { $($name( $ty )),+ } | ||||
|         pub struct Error { message: String, error: ErrorKind } | ||||
|   | ||||
| @@ -53,6 +53,17 @@ | ||||
|             </form> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div id="config-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow"> | ||||
|         <div> | ||||
|             <h6 class="mb-0 text-white">Configuration</h6> | ||||
|  | ||||
|             <form class="form" id="config-form"> | ||||
|                 <textarea id="config-text" class="form-control" style="height: 300px;">{{config}}</textarea> | ||||
|                 <button type="submit" class="btn btn-primary">Save</button> | ||||
|             </form> | ||||
|         </div> | ||||
|     </div> | ||||
| </main> | ||||
|  | ||||
| <script> | ||||
| @@ -91,12 +102,18 @@ | ||||
|     } | ||||
|     function inviteUser() { | ||||
|         inv = $("#email-invite"); | ||||
|         data = JSON.stringify({ "Email": inv.val() }); | ||||
|         data = JSON.stringify({ "email": inv.val() }); | ||||
|         inv.val(""); | ||||
|         _post("/admin/invite/", "User invited correctly", | ||||
|             "Error inviting user", data); | ||||
|         return false; | ||||
|     } | ||||
|     function saveConfig() { | ||||
|         data = $("#config-text").val(); | ||||
|         _post("/admin/config/", "Config saved correctly", | ||||
|             "Error saving config", data); | ||||
|         return false; | ||||
|     } | ||||
|     let OrgTypes = { | ||||
|         "0": { "name": "Owner", "color": "orange" }, | ||||
|         "1": { "name": "Admin", "color": "blueviolet" }, | ||||
| @@ -105,6 +122,7 @@ | ||||
|     }; | ||||
|     $(window).on('load', function () { | ||||
|         $("#invite-form").submit(inviteUser); | ||||
|         $("#config-form").submit(saveConfig); | ||||
|         $("img.identicon").each(function (i, e) { | ||||
|             e.src = identicon(e.dataset.src); | ||||
|         }); | ||||
|   | ||||
							
								
								
									
										31
									
								
								src/util.rs
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								src/util.rs
									
									
									
									
									
								
							| @@ -77,6 +77,15 @@ pub fn read_file(path: &str) -> IOResult<Vec<u8>> { | ||||
|     Ok(contents) | ||||
| } | ||||
|  | ||||
| pub fn read_file_string(path: &str) -> IOResult<String> { | ||||
|     let mut contents = String::new(); | ||||
|  | ||||
|     let mut file = File::open(Path::new(path))?; | ||||
|     file.read_to_string(&mut contents)?; | ||||
|  | ||||
|     Ok(contents) | ||||
| } | ||||
|  | ||||
| pub fn delete_file(path: &str) -> IOResult<()> { | ||||
|     let res = fs::remove_file(path); | ||||
|  | ||||
| @@ -284,25 +293,3 @@ where | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| // | ||||
| // Into Result | ||||
| // | ||||
| use crate::error::Error; | ||||
|  | ||||
| pub trait IntoResult<T> { | ||||
|     fn into_result(self) -> Result<T, Error>; | ||||
| } | ||||
|  | ||||
| impl<T> IntoResult<T> for Result<T, Error> { | ||||
|     fn into_result(self) -> Result<T, Error> { | ||||
|         self | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<T> IntoResult<T> for T { | ||||
|     fn into_result(self) -> Result<T, Error> { | ||||
|         Ok(self) | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user