Skip to content
Snippets Groups Projects
Unverified Commit 877408b8 authored by Daniel García's avatar Daniel García
Browse files

Implement basic config loading and updating. No save to file yet.

parent 86ed75bf
No related branches found
No related tags found
No related merge requests found
......@@ -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
......
......@@ -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);
});
......
......@@ -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)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment