Add groups

This commit is contained in:
Daniel García 2019-02-05 22:17:02 +01:00
parent dc92f07232
commit 9976e4736e
No known key found for this signature in database
GPG Key ID: FC8A7D14C3CD543A
3 changed files with 169 additions and 134 deletions

View File

@ -13,8 +13,17 @@ lazy_static! {
} }
macro_rules! make_config { macro_rules! make_config {
( $( $(#[doc = $doc:literal])+ $name:ident : $ty:ty, $editable:literal, $none_action:ident $(, $default:expr)? );+ $(;)? ) => { (
$(
$(#[doc = $groupdoc:literal])?
$group:ident {
$(
$(#[doc = $doc:literal])+
$name:ident : $ty:ty, $editable:literal, $none_action:ident $(, $default:expr)?;
)+
},)+
) => {
pub struct Config { inner: RwLock<Inner> } pub struct Config { inner: RwLock<Inner> }
struct Inner { struct Inner {
@ -27,10 +36,10 @@ macro_rules! make_config {
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ConfigBuilder { pub struct ConfigBuilder {
$( $($(
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
$name: Option<$ty> $name: Option<$ty>,
),+ )+)+
} }
impl ConfigBuilder { impl ConfigBuilder {
@ -38,9 +47,9 @@ macro_rules! make_config {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
let mut builder = ConfigBuilder::default(); let mut builder = ConfigBuilder::default();
$( $($(
builder.$name = get_env(&stringify!($name).to_uppercase()); builder.$name = get_env(&stringify!($name).to_uppercase());
)+ )+)+
builder builder
} }
@ -55,11 +64,11 @@ macro_rules! make_config {
/// If both have the same element, `other` wins. /// If both have the same element, `other` wins.
fn merge(&self, other: &Self) -> Self { fn merge(&self, other: &Self) -> Self {
let mut builder = self.clone(); let mut builder = self.clone();
$( $($(
if let v @Some(_) = &other.$name { if let v @Some(_) = &other.$name {
builder.$name = v.clone(); builder.$name = v.clone();
} }
)+ )+)+
builder builder
} }
@ -67,21 +76,21 @@ macro_rules! make_config {
/// except those that are equal in both sides /// except those that are equal in both sides
fn remove(&self, other: &Self) -> Self { fn remove(&self, other: &Self) -> Self {
let mut builder = ConfigBuilder::default(); let mut builder = ConfigBuilder::default();
$( $($(
if &self.$name != &other.$name { if &self.$name != &other.$name {
builder.$name = self.$name.clone(); builder.$name = self.$name.clone();
} }
)+ )+)+
builder builder
} }
fn build(&self) -> ConfigItems { fn build(&self) -> ConfigItems {
let mut config = ConfigItems::default(); let mut config = ConfigItems::default();
let _domain_set = self.domain.is_some(); let _domain_set = self.domain.is_some();
$( $($(
config.$name = make_config!{ @build self.$name.clone(), &config, $none_action, $($default)? }; config.$name = make_config!{ @build self.$name.clone(), &config, $none_action, $($default)? };
)+ )+)+
config.domain_set = _domain_set; config.domain_set = _domain_set;
config config
@ -89,15 +98,15 @@ macro_rules! make_config {
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ConfigItems { $(pub $name: make_config!{@type $ty, $none_action} ),+ } pub struct ConfigItems { $($(pub $name: make_config!{@type $ty, $none_action}, )+)+ }
#[allow(unused)] #[allow(unused)]
impl Config { impl Config {
$( $($(
pub fn $name(&self) -> make_config!{@type $ty, $none_action} { pub fn $name(&self) -> make_config!{@type $ty, $none_action} {
self.inner.read().unwrap().config.$name.clone() self.inner.read().unwrap().config.$name.clone()
} }
)+ )+)+
pub fn load() -> Result<Self, Error> { pub fn load() -> Result<Self, Error> {
// Loading from env and file // Loading from env and file
@ -122,9 +131,9 @@ macro_rules! make_config {
} }
pub fn prepare_json(&self) -> serde_json::Value { pub fn prepare_json(&self) -> serde_json::Value {
let cfg = { let (def, cfg) = {
let inner = &self.inner.read().unwrap(); let inner = &self.inner.read().unwrap();
inner._env.merge(&inner._usr) (inner._env.build(), inner.config.clone())
}; };
@ -139,24 +148,32 @@ macro_rules! make_config {
fn _get_doc(doc: &str) -> serde_json::Value { fn _get_doc(doc: &str) -> serde_json::Value {
let mut split = doc.split("|>").map(str::trim); let mut split = doc.split("|>").map(str::trim);
json!({ json!({
"group": split.next(),
"name": split.next(), "name": split.next(),
"description": split.next() "description": split.next()
}) })
} }
json!([ $( { json!([ $({
"group": stringify!($group),
"groupdoc": make_config!{ @show $($groupdoc)? },
"elements": [
$( {
"editable": $editable, "editable": $editable,
"name": stringify!($name), "name": stringify!($name),
"value": cfg.$name, "value": cfg.$name,
"default": make_config!{ @default &cfg, $none_action, $($default)? }, "default": def.$name,
"type": _get_form_type(stringify!($ty)), "type": _get_form_type(stringify!($ty)),
"doc": _get_doc(concat!($($doc),+)), "doc": _get_doc(concat!($($doc),+)),
}, )+ ]) }, )+
]}, )+ ])
} }
} }
}; };
// Group or empty string
( @show ) => { "" };
( @show $groupdoc:literal ) => { $groupdoc };
// Wrap the optionals in an Option type // Wrap the optionals in an Option type
( @type $ty:ty, option) => { Option<$ty> }; ( @type $ty:ty, option) => { Option<$ty> };
( @type $ty:ty, $id:ident) => { $ty }; ( @type $ty:ty, $id:ident) => { $ty };
@ -173,108 +190,115 @@ macro_rules! make_config {
} }
} }
}}; }};
// Get a default value
( @default $config:expr, option, ) => { serde_json::Value::Null };
( @default $config:expr, def, $default:expr ) => { $default };
( @default $config:expr, auto, $default_fn:expr ) => {{
let f: &Fn(ConfigItems) -> _ = &$default_fn;
f($config.build())
}};
} }
//STRUCTURE: //STRUCTURE:
// /// Group |> Friendly Name |> Description (Optional) // /// Short description (without this they won't appear on the list)
// group {
// /// Friendly Name |> Description (Optional)
// name: type, is_editable, none_action, <default_value (Optional)> // name: type, is_editable, none_action, <default_value (Optional)>
// }
// //
// Where none_action applied when the value wasn't provided and can be: // Where none_action applied when the value wasn't provided and can be:
// def: Use a default value // def: Use a default value
// auto: Value is auto generated based on other values // auto: Value is auto generated based on other values
// option: Value is optional // option: Value is optional
make_config! { make_config! {
/// folders |> Data folder |> Main data folder folders {
/// Data folder |> Main data folder
data_folder: String, false, def, "data".to_string(); data_folder: String, false, def, "data".to_string();
/// folders |> Database URL /// Database URL
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3"); database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
/// folders |> Icon chache folder /// Icon chache folder
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache"); icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
/// folders |> Attachments folder /// Attachments folder
attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments"); attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
/// folders |> Templates folder /// Templates folder
templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates"); templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
/// folders |> Session JWT key /// Session JWT key
rsa_key_filename: String, false, auto, |c| format!("{}/{}", c.data_folder, "rsa_key"); rsa_key_filename: String, false, auto, |c| format!("{}/{}", c.data_folder, "rsa_key");
/// Web vault folder
/// ws |> Enable websocket notifications
websocket_enabled: bool, false, def, false;
/// ws |> Websocket address
websocket_address: String, false, def, "0.0.0.0".to_string();
/// ws |> Websocket port
websocket_port: u16, false, def, 3012;
/// folders |> Web vault folder
web_vault_folder: String, false, def, "web-vault/".to_string(); web_vault_folder: String, false, def, "web-vault/".to_string();
/// settings |> Enable web vault },
ws {
/// Enable websocket notifications
websocket_enabled: bool, false, def, false;
/// Websocket address
websocket_address: String, false, def, "0.0.0.0".to_string();
/// Websocket port
websocket_port: u16, false, def, 3012;
},
/// General settings
settings {
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' and port, if it's different than the default. Some server functions don't work correctly without this value
domain: String, true, def, "http://localhost".to_string();
/// PRIVATE |> Domain set
domain_set: bool, false, def, false;
/// Enable web vault
web_vault_enabled: bool, false, def, true; web_vault_enabled: bool, false, def, true;
/// icons |> Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded /// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
icon_cache_ttl: u64, true, def, 2_592_000;
/// icons |> Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
icon_cache_negttl: u64, true, def, 259_200;
/// settings |> Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
/// but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0, /// but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
/// otherwise it will delete them and they won't be downloaded again. /// otherwise it will delete them and they won't be downloaded again.
disable_icon_download: bool, true, def, false; disable_icon_download: bool, true, def, false;
/// settings |> Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited /// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
signups_allowed: bool, true, def, true; signups_allowed: bool, true, def, true;
/// settings |> Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
invitations_allowed: bool, true, def, true; invitations_allowed: bool, true, def, true;
/// settings |> Password iterations |> Number of server-side passwords hashing iterations. The changes only apply when a user changes their password. Not recommended to lower the value /// Password iterations |> Number of server-side passwords hashing iterations. The changes only apply when a user changes their password. Not recommended to lower the value
password_iterations: i32, true, def, 100_000; password_iterations: i32, true, def, 100_000;
/// settings |> Show password hints |> Controls if the password hint should be shown directly in the web page. Otherwise, if email is disabled, there is no way to see the password hint /// Show password hints |> Controls if the password hint should be shown directly in the web page. Otherwise, if email is disabled, there is no way to see the password hint
show_password_hint: bool, true, def, true; show_password_hint: bool, true, def, true;
/// settings |> Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' and port, if it's different than the default. Some server functions don't work correctly without this value /// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
domain: String, true, def, "http://localhost".to_string(); admin_token: String, true, option;
/// private |> Domain set },
domain_set: bool, false, def, false;
/// settings |> Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request. ONLY use this during development, as it can slow down the server /// Advanced settings
advanced {
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
icon_cache_ttl: u64, true, def, 2_592_000;
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
icon_cache_negttl: u64, true, def, 259_200;
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request. ONLY use this during development, as it can slow down the server
reload_templates: bool, true, def, false; reload_templates: bool, true, def, false;
/// log |> Enable extended logging /// Enable extended logging
extended_logging: bool, false, def, true; extended_logging: bool, false, def, true;
/// log |> Log file path /// Log file path
log_file: String, false, option; log_file: String, false, option;
},
/// settings |> Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session /// Yubikey settings
admin_token: String, true, option; yubico {
/// Client ID
/// yubico |> Yubico Client ID
yubico_client_id: String, true, option; yubico_client_id: String, true, option;
/// yubico |> Yubico secret Key /// Secret Key
yubico_secret_key: String, true, option; yubico_secret_key: String, true, option;
/// yubico |> Yubico Server /// Server
yubico_server: String, true, option; yubico_server: String, true, option;
},
// TODO: Remove SMTP from name once groups work /// SMTP Email Settings
/// mail |> SMTP Host smtp {
/// Host
smtp_host: String, true, option; smtp_host: String, true, option;
/// mail |> Enable SMTP SSL /// Enable SSL
smtp_ssl: bool, true, def, true; smtp_ssl: bool, true, def, true;
/// mail |> SMTP Port /// Port
smtp_port: u16, true, auto, |c| if c.smtp_ssl {587} else {25}; smtp_port: u16, true, auto, |c| if c.smtp_ssl {587} else {25};
/// mail |> SMTP From Address /// From Address
smtp_from: String, true, def, String::new(); smtp_from: String, true, def, String::new();
/// mail |> SMTP From Name /// From Name
smtp_from_name: String, true, def, "Bitwarden_RS".to_string(); smtp_from_name: String, true, def, "Bitwarden_RS".to_string();
/// mail |> SMTP Username /// Username
smtp_username: String, true, option; smtp_username: String, true, option;
/// mail |> SMTP Password /// Password
smtp_password: String, true, option; smtp_password: String, true, option;
},
} }
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {

View File

@ -14,7 +14,8 @@
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/identicon.js/2.3.3/identicon.min.js" integrity="sha256-nYoL3nK/HA1e1pJvLwNPnpKuKG9q89VFX862r5aohmA=" <script src="https://cdnjs.cloudflare.com/ajax/libs/identicon.js/2.3.3/identicon.min.js" integrity="sha256-nYoL3nK/HA1e1pJvLwNPnpKuKG9q89VFX862r5aohmA="
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.2.1/js/bootstrap.bundle.min.js" integrity="sha256-MSYVjWgrr6UL/9eQfQvOyt6/gsxb6dpwI1zqM5DbLCs="
crossorigin="anonymous"></script>
<style> <style>
body { body {
padding-top: 70px; padding-top: 70px;

View File

@ -54,11 +54,17 @@
</div> </div>
</div> </div>
<div id="config-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow"> <div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow">
<div> <div>
<h6 class="text-white">Configuration</h6> <h6 class="text-white mb-3">Configuration</h6>
<form class="form" id="config-form"> <form class="form accordion" id="config-form">
{{#each config}} {{#each config}}
{{#if groupdoc}}
<div class="card bg-light mb-3">
<div class="card-header"><button class="btn btn-link collapsed" type="button" data-toggle="collapse"
data-target="#g_{{group}}">{{groupdoc}}</button></div>
<div id="g_{{group}}" class="card-body collapse" data-parent="#config-form">
{{#each elements}}
{{#if editable}} {{#if editable}}
<div class="form-group row" title="{{doc.description}}"> <div class="form-group row" title="{{doc.description}}">
{{#case type "text" "number"}} {{#case type "text" "number"}}
@ -82,6 +88,10 @@
</div> </div>
{{/if}} {{/if}}
{{/each}} {{/each}}
</div>
</div>
{{/if}}
{{/each}}
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
</form> </form>
</div> </div>