Merge branch 'cleanup-0.4.3' into 'development'

synchronize default values with documentation; add more precise help descriptions

See merge request fabinfra/fabaccess/bffh!53
This commit is contained in:
Mario Voigt 2025-03-06 01:32:09 +01:00
commit 66c830bbf3
14 changed files with 231 additions and 90 deletions

View File

@ -243,12 +243,16 @@ pub fn load(
.compat(), .compat(),
); );
let mut actor_map: HashMap<String, _> = config let mut actor_connections_data_vec: Vec<(String, String)> = vec![];
.actor_connections for actor_connection in config.actor_connections.clone().into_iter() {
actor_connections_data_vec.push((actor_connection.machine, actor_connection.actor));
}
let mut actor_map: HashMap<String, _> = actor_connections_data_vec
.iter() .iter()
.filter_map(|(k, v)| { .filter_map(|(k, v)| {
if let Some(resource) = resources.get_by_id(v) { if let Some(resource) = resources.get_by_id(k) {
Some((k.clone(), resource.get_signal())) Some((v.clone(), resource.get_signal()))
} else { } else {
tracing::error!(actor=%k, machine=%v, "Machine configured for actor not found!"); tracing::error!(actor=%k, machine=%v, "Machine configured for actor not found!");
None None

View File

@ -32,7 +32,7 @@ pub struct PrivilegesBuf {
// i.e. "bffh.perm" is not the same as "bffհ.реrm" (Armenian 'հ':Հ and Cyrillic 'е':Е) // i.e. "bffh.perm" is not the same as "bffհ.реrm" (Armenian 'հ':Հ and Cyrillic 'е':Е)
// See also https://util.unicode.org/UnicodeJsps/confusables.jsp // See also https://util.unicode.org/UnicodeJsps/confusables.jsp
pub struct PermissionBuf { pub struct PermissionBuf {
inner: String, pub inner: String,
} }
impl PermissionBuf { impl PermissionBuf {
#[inline(always)] #[inline(always)]

View File

@ -131,11 +131,11 @@ pub struct Role {
/// This makes situations where different levels of access are required easier: Each higher /// This makes situations where different levels of access are required easier: Each higher
/// level of access sets the lower levels of access as parent, inheriting their permission; if /// level of access sets the lower levels of access as parent, inheriting their permission; if
/// you are allowed to manage a machine you are then also allowed to use it and so on /// you are allowed to manage a machine you are then also allowed to use it and so on
parents: Vec<String>, pub parents: Vec<String>,
// If a role doesn't define permissions, default to an empty Vec. // If a role doesn't define permissions, default to an empty Vec.
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
permissions: Vec<PermRule>, pub permissions: Vec<PermRule>,
} }
impl Role { impl Role {

View File

@ -143,7 +143,8 @@ impl admin::Server for User {
// Only update if needed // Only update if needed
if !target.userdata.roles.iter().any(|r| r.as_str() == rolename) { if !target.userdata.roles.iter().any(|r| r.as_str() == rolename) {
target.userdata.roles.push(rolename.to_string()); target.userdata.roles.push(rolename.to_string());
pry!(self.session pry!(self
.session
.users .users
.put_user(self.user.get_username(), &target)); .put_user(self.user.get_username(), &target));
} }
@ -168,7 +169,8 @@ impl admin::Server for User {
// Only update if needed // Only update if needed
if target.userdata.roles.iter().any(|r| r.as_str() == rolename) { if target.userdata.roles.iter().any(|r| r.as_str() == rolename) {
target.userdata.roles.retain(|r| r.as_str() != rolename); target.userdata.roles.retain(|r| r.as_str() != rolename);
pry!(self.session pry!(self
.session
.users .users
.put_user(self.user.get_username(), &target)); .put_user(self.user.get_username(), &target));
} }

View File

@ -5,7 +5,7 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::authorization::permissions::PrivilegesBuf; use crate::authorization::permissions::{PermRule, PermissionBuf, PrivilegesBuf};
use crate::authorization::roles::Role; use crate::authorization::roles::Role;
use crate::capnp::{Listen, TlsListen}; use crate::capnp::{Listen, TlsListen};
use crate::logging::LogConfig; use crate::logging::LogConfig;
@ -60,28 +60,13 @@ pub struct MachineDescription {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub spacename: String,
pub instanceurl: String,
/// A list of address/port pairs to listen on. /// A list of address/port pairs to listen on.
pub listens: Vec<Listen>, pub listens: Vec<Listen>,
/// Machine descriptions to load
pub machines: HashMap<String, MachineDescription>,
/// Actors to load and their configuration options
pub actors: HashMap<String, ModuleConfig>,
/// Initiators to load and their configuration options
pub initiators: HashMap<String, ModuleConfig>,
pub mqtt_url: String,
pub actor_connections: Vec<(String, String)>,
pub init_connections: Vec<(String, String)>,
pub db_path: PathBuf,
pub auditlog_path: PathBuf,
pub roles: HashMap<String, Role>,
#[serde(flatten)] #[serde(flatten)]
pub tlsconfig: TlsListen, pub tlsconfig: TlsListen,
@ -94,9 +79,22 @@ pub struct Config {
#[serde(default, skip)] #[serde(default, skip)]
pub logging: LogConfig, pub logging: LogConfig,
pub spacename: String, pub mqtt_url: String,
pub db_path: PathBuf,
pub auditlog_path: PathBuf,
pub instanceurl: String, pub roles: HashMap<String, Role>,
/// Machine descriptions to load
pub machines: HashMap<String, MachineDescription>,
/// Actors to load and their configuration options
pub actors: HashMap<String, ModuleConfig>,
pub actor_connections: Vec<ActorConnectionConfig>,
/// Initiators to load and their configuration options
pub initiators: HashMap<String, ModuleConfig>,
pub init_connections: Vec<InitiatorConnectionConfig>,
} }
impl Config { impl Config {
@ -111,6 +109,24 @@ pub struct ModuleConfig {
pub params: HashMap<String, String>, pub params: HashMap<String, String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParamsConfig {
pub module: String,
pub params: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActorConnectionConfig {
pub machine: String,
pub actor: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitiatorConnectionConfig {
pub machine: String,
pub initiator: String,
}
pub(crate) fn deser_option<'de, D, T>(d: D) -> std::result::Result<Option<T>, D::Error> pub(crate) fn deser_option<'de, D, T>(d: D) -> std::result::Result<Option<T>, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@ -123,50 +139,151 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
let mut actors: HashMap<String, ModuleConfig> = HashMap::new(); let mut actors: HashMap<String, ModuleConfig> = HashMap::new();
let mut initiators: HashMap<String, ModuleConfig> = HashMap::new(); let mut initiators: HashMap<String, ModuleConfig> = HashMap::new();
let machines = HashMap::new(); let mut roles: HashMap<String, Role> = HashMap::new();
let mut machines: HashMap<String, MachineDescription> = HashMap::new();
let mut initiator_123_params: HashMap<String, String> = HashMap::new();
initiator_123_params.insert("args".to_string(), "".to_string());
initiator_123_params.insert("cmd".to_string(), "echo".to_string());
let mut actor_connections_vec: Vec<ActorConnectionConfig> = vec![ActorConnectionConfig {
machine: "resource_a".to_string(),
actor: "actor_123".to_string(),
}];
let mut initiator_connections_vec: Vec<InitiatorConnectionConfig> =
vec![InitiatorConnectionConfig {
machine: "resource_a".to_string(),
initiator: "initiator_123".to_string(),
}];
roles.insert(
"admin".to_string(),
Role {
parents: Vec::new(),
permissions: vec![
PermRule::Base(PermissionBuf {
inner: "bffh.users.info".to_string(),
}),
PermRule::Base(PermissionBuf {
inner: "bffh.users.manage".to_string(),
}),
PermRule::Base(PermissionBuf {
inner: "bffh.users.admin".to_string(),
}),
],
},
);
roles.insert(
"member".to_string(),
Role {
parents: Vec::new(),
permissions: vec![
PermRule::Base(PermissionBuf {
inner: "lab.some.disclose".to_string(),
}),
PermRule::Base(PermissionBuf {
inner: "lab.some.read".to_string(),
}),
PermRule::Base(PermissionBuf {
inner: "lab.some.write".to_string(),
}),
PermRule::Base(PermissionBuf {
inner: "lab.some.manage".to_string(),
}),
],
},
);
machines.insert(
"resource_a".to_string(),
MachineDescription {
name: "Resource A".to_string(),
description: Option::from("A description".to_string()),
wiki: Option::from("https://some.wiki.url".to_string()),
category: Option::from("A category".to_string()),
privs: PrivilegesBuf {
disclose: PermissionBuf {
inner: "lab.some.disclose".to_string(),
},
read: PermissionBuf {
inner: "lab.some.read".to_string(),
},
write: PermissionBuf {
inner: "lab.some.write".to_string(),
},
manage: PermissionBuf {
inner: "lab.some.manage".to_string(),
},
},
},
);
machines.insert(
"resource_b".to_string(),
MachineDescription {
name: "Resource B".to_string(),
description: Option::from("A description".to_string()),
wiki: Option::from("https://some.wiki.url".to_string()),
category: Option::from("A category".to_string()),
privs: PrivilegesBuf {
disclose: PermissionBuf {
inner: "lab.some.disclose".to_string(),
},
read: PermissionBuf {
inner: "lab.some.read".to_string(),
},
write: PermissionBuf {
inner: "lab.some.write".to_string(),
},
manage: PermissionBuf {
inner: "lab.some.manage".to_string(),
},
},
},
);
actors.insert( actors.insert(
"Actor".to_string(), "actor_123".to_string(),
ModuleConfig { ModuleConfig {
module: "Shelly".to_string(), module: "Shelly".to_string(),
params: HashMap::new(), params: HashMap::new(),
}, },
); );
initiators.insert( initiators.insert(
"Initiator".to_string(), "initiator_123".to_string(),
ModuleConfig { ModuleConfig {
module: "TCP-Listen".to_string(), module: "Process".to_string(),
params: HashMap::new(), params: initiator_123_params,
}, },
); );
Config { Config {
spacename: "fabaccess.sample.space".into(),
instanceurl: "https://fabaccess.sample.space".into(),
listens: vec![Listen { listens: vec![Listen {
address: "127.0.0.1".to_string(), address: "127.0.0.1".to_string(),
port: None, port: None,
}], }],
actors,
initiators,
machines,
mqtt_url: "tcp://localhost:1883".to_string(),
actor_connections: vec![("Testmachine".to_string(), "Actor".to_string())],
init_connections: vec![("Initiator".to_string(), "Testmachine".to_string())],
db_path: PathBuf::from("/run/bffh/database"),
auditlog_path: PathBuf::from("/var/log/bffh/audit.log"),
roles: HashMap::new(),
tlsconfig: TlsListen { tlsconfig: TlsListen {
certfile: PathBuf::from("./bffh.crt"), certfile: PathBuf::from("/etc/bffh/certs/bffh.crt"),
keyfile: PathBuf::from("./bffh.key"), keyfile: PathBuf::from("/etc/bffh/certs/bffh.key"),
..Default::default() ..Default::default()
}, },
tlskeylog: None, tlskeylog: None,
verbosity: 0, verbosity: 0,
logging: LogConfig::default(), logging: LogConfig::default(),
instanceurl: "".into(), mqtt_url: "mqtt://127.0.0.1:1883".to_string(),
spacename: "".into(), db_path: PathBuf::from("/var/lib/bffh/bffh.db"),
auditlog_path: PathBuf::from("/var/log/bffh/audit.json"),
roles,
machines,
actors,
actor_connections: actor_connections_vec,
initiators,
init_connections: initiator_connections_vec,
} }
} }
} }

View File

@ -2,9 +2,7 @@ use crate::initiators::dummy::Dummy;
use crate::initiators::process::Process; use crate::initiators::process::Process;
use crate::resources::modules::fabaccess::Status; use crate::resources::modules::fabaccess::Status;
use crate::session::SessionHandle; use crate::session::SessionHandle;
use crate::{ use crate::{AuthenticationHandle, Config, Resource, ResourcesHandle, SessionManager};
AuthenticationHandle, Config, Resource, ResourcesHandle, SessionManager,
};
use executor::prelude::Executor; use executor::prelude::Executor;
use futures_util::ready; use futures_util::ready;
use std::collections::HashMap; use std::collections::HashMap;
@ -105,12 +103,16 @@ pub fn load(
let span = tracing::info_span!("loading initiators"); let span = tracing::info_span!("loading initiators");
let _guard = span.enter(); let _guard = span.enter();
let mut initiator_map: HashMap<String, Resource> = config let mut init_connections_data_vec: Vec<(String, String)> = vec![];
.init_connections for init_connection in config.init_connections.clone().into_iter() {
init_connections_data_vec.push((init_connection.machine, init_connection.initiator));
}
let mut initiator_map: HashMap<String, Resource> = init_connections_data_vec
.iter() .iter()
.filter_map(|(k, v)| { .filter_map(|(k, v)| {
if let Some(resource) = resources.get_by_id(v) { if let Some(resource) = resources.get_by_id(k) {
Some((k.clone(), resource.clone())) Some((v.clone(), resource.clone()))
} else { } else {
tracing::error!(initiator=%k, machine=%v, tracing::error!(initiator=%k, machine=%v,
"Machine configured for initiator not found!"); "Machine configured for initiator not found!");

View File

@ -208,9 +208,11 @@ impl Difluoroborane {
pub fn dump_db(&mut self, file: &str) -> Result<(), miette::Error> { pub fn dump_db(&mut self, file: &str) -> Result<(), miette::Error> {
let users = self.users.dump_map()?; let users = self.users.dump_map()?;
let state = self.statedb.dump_map()?; let state = self.statedb.dump_map()?;
let dump = DatabaseDump{users, state}; let dump = DatabaseDump { users, state };
let data = toml::ser::to_vec(&dump).map_err(|e| miette::Error::msg(format!("Serializing database dump failed: {}", e)))?; let data = toml::ser::to_vec(&dump)
std::fs::write(file, &data).map_err(|e| miette::Error::msg(format!("writing database dump failed: {}", e)))?; .map_err(|e| miette::Error::msg(format!("Serializing database dump failed: {}", e)))?;
std::fs::write(file, &data)
.map_err(|e| miette::Error::msg(format!("writing database dump failed: {}", e)))?;
Ok(()) Ok(())
} }
@ -236,7 +238,8 @@ impl Difluoroborane {
self.resources.clone(), self.resources.clone(),
sessionmanager.clone(), sessionmanager.clone(),
authentication.clone(), authentication.clone(),
).expect("initializing initiators failed"); )
.expect("initializing initiators failed");
// TODO 0.5: error handling. Add variant to BFFHError // TODO 0.5: error handling. Add variant to BFFHError
actors::load(self.executor.clone(), &self.config, self.resources.clone())?; actors::load(self.executor.clone(), &self.config, self.resources.clone())?;

View File

@ -90,7 +90,11 @@ impl Inner {
.unwrap() .unwrap()
.log(self.id.as_str(), &format!("{}", state)); .log(self.id.as_str(), &format!("{}", state));
if let Err(e) = res { if let Err(e) = res {
tracing::error!("Writing to the audit log failed for {} {}: {e}", self.id.as_str(), state); tracing::error!(
"Writing to the audit log failed for {} {}: {e}",
self.id.as_str(),
state
);
} }
self.signal.set(state); self.signal.set(state);
@ -164,7 +168,9 @@ impl Resource {
fn set_state(&self, state: MachineState) { fn set_state(&self, state: MachineState) {
let mut serializer = AllocSerializer::<1024>::default(); let mut serializer = AllocSerializer::<1024>::default();
serializer.serialize_value(&state).expect("serializing a MachineState shoud be infallible"); serializer
.serialize_value(&state)
.expect("serializing a MachineState shoud be infallible");
let archived = ArchivedValue::new(serializer.into_serializer().into_inner()); let archived = ArchivedValue::new(serializer.into_serializer().into_inner());
self.inner.set_state(archived) self.inner.set_state(archived)
} }

View File

@ -1,5 +1,5 @@
use rkyv::ser::Serializer;
use rkyv::ser::serializers::AllocSerializer; use rkyv::ser::serializers::AllocSerializer;
use rkyv::ser::Serializer;
use thiserror::Error; use thiserror::Error;
use crate::db; use crate::db;
@ -54,8 +54,7 @@ impl StateDB {
} }
pub fn open_with_env(env: Arc<Environment>) -> Result<Self, StateDBError> { pub fn open_with_env(env: Arc<Environment>) -> Result<Self, StateDBError> {
let db = RawDB::open(&env, Some("state")) let db = RawDB::open(&env, Some("state")).map_err(|e| StateDBError::Open(e.into()))?;
.map_err(|e| StateDBError::Open(e.into()))?;
Ok(Self::new(env, db)) Ok(Self::new(env, db))
} }
@ -117,8 +116,11 @@ impl StateDB {
pub fn dump_map(&self) -> miette::Result<std::collections::HashMap<String, State>> { pub fn dump_map(&self) -> miette::Result<std::collections::HashMap<String, State>> {
let mut map = std::collections::HashMap::new(); let mut map = std::collections::HashMap::new();
for (key, val) in self.get_all(&self.begin_ro_txn()?)? { for (key, val) in self.get_all(&self.begin_ro_txn()?)? {
let key_str = core::str::from_utf8(&key).map_err(|_e| miette::Error::msg("state key not UTF8"))?.to_string(); let key_str = core::str::from_utf8(&key)
let val_state: State = rkyv::Deserialize::deserialize(val.as_ref(), &mut rkyv::Infallible).unwrap(); .map_err(|_e| miette::Error::msg("state key not UTF8"))?
.to_string();
let val_state: State =
rkyv::Deserialize::deserialize(val.as_ref(), &mut rkyv::Infallible).unwrap();
map.insert(key_str, val_state); map.insert(key_str, val_state);
} }
Ok(map) Ok(map)

View File

@ -1,5 +1,5 @@
use std::fmt::{Debug, Display, Formatter};
use std::fmt; use std::fmt;
use std::fmt::{Debug, Display, Formatter};
use std::ops::Deref; use std::ops::Deref;

View File

@ -173,7 +173,7 @@ impl Users {
Ok(()) Ok(())
} }
pub fn load_map(&mut self, dump: &HashMap<String,UserData>) -> miette::Result<()> { pub fn load_map(&mut self, dump: &HashMap<String, UserData>) -> miette::Result<()> {
let mut txn = unsafe { self.userdb.get_rw_txn() }?; let mut txn = unsafe { self.userdb.get_rw_txn() }?;
self.userdb.clear_txn(&mut txn)?; self.userdb.clear_txn(&mut txn)?;
@ -194,7 +194,7 @@ impl Users {
} }
pub fn dump_map(&self) -> miette::Result<HashMap<String, UserData>> { pub fn dump_map(&self) -> miette::Result<HashMap<String, UserData>> {
return Ok(self.userdb.get_all()?) return Ok(self.userdb.get_all()?);
} }
pub fn dump_file(&self, path_str: &str, force: bool) -> miette::Result<usize> { pub fn dump_file(&self, path_str: &str, force: bool) -> miette::Result<usize> {
let path = Path::new(path_str); let path = Path::new(path_str);

View File

@ -23,12 +23,12 @@ fn main() -> miette::Result<()> {
build_kind=difluoroborane::env::BUILD_RUST_CHANNEL)) build_kind=difluoroborane::env::BUILD_RUST_CHANNEL))
.about(clap::crate_description!()) .about(clap::crate_description!())
.arg(Arg::new("config") .arg(Arg::new("config")
.help("Path to the config file to use") .help("Path to the DHALL config file to use")
.long("config") .long("config")
.short('c') .short('c')
.takes_value(true)) .takes_value(true))
.arg(Arg::new("verbosity") .arg(Arg::new("verbosity")
.help("Increase logging verbosity") .help("Increase logging verbosity. Stackable from -v up to -vvv")
.long("verbose") .long("verbose")
.short('v') .short('v')
.multiple_occurrences(true) .multiple_occurrences(true)
@ -47,18 +47,19 @@ fn main() -> miette::Result<()> {
.arg(Arg::new("log level") .arg(Arg::new("log level")
.help("Set the desired log levels.") .help("Set the desired log levels.")
.long("log-level") .long("log-level")
.takes_value(true)) .takes_value(true)
.possible_values(["info", "warn", "error", "debug", "trace"]))
.arg( .arg(
Arg::new("print default") Arg::new("print default")
.help("Print a default config to stdout instead of running") .help("Print a default DHALL config to stdout instead of running")
.long("print-default")) .long("print-default"))
.arg( .arg(
Arg::new("check config") Arg::new("check config")
.help("Check config for validity") .help("Check DHALL config for validity")
.long("check")) .long("check"))
.arg( .arg(
Arg::new("dump-db") Arg::new("dump-db")
.help("Dump all internal databases") .help("Dump all internal databases (states and users) to the given file as TOML")
.long("dump-db") .long("dump-db")
.alias("dump") .alias("dump")
.conflicts_with("dump-users") .conflicts_with("dump-users")
@ -83,29 +84,31 @@ fn main() -> miette::Result<()> {
) )
.arg( .arg(
Arg::new("force") Arg::new("force")
.help("force ops that may clobber") .help("Force owerwriting existing files")
.long("force") .long("force")
) )
.arg( .arg(
Arg::new("load-users") Arg::new("load-users")
.help("Load users into the internal databases") .help("Load users from TOML into the internal databases")
.long("load-users") .long("load-users")
.alias("load") .alias("load")
.takes_value(true) .takes_value(true)
.value_name("FILE")
.conflicts_with("dump-db") .conflicts_with("dump-db")
.conflicts_with("load-db") .conflicts_with("load-db")
.conflicts_with("dump-users") .conflicts_with("dump-users")
) )
.arg( .arg(
Arg::new("load-db") Arg::new("load-db")
.help("Load values into the internal databases") .help("Load values from TOML into the internal databases")
.long("load-db") .long("load-db")
.takes_value(true) .takes_value(true)
.value_name("FILE")
.conflicts_with("dump-db") .conflicts_with("dump-db")
.conflicts_with("load-users") .conflicts_with("load-users")
.conflicts_with("dump-users")) .conflicts_with("dump-users"))
.arg(Arg::new("keylog") .arg(Arg::new("keylog")
.help("log TLS keys into PATH. If no path is specified the value of the envvar SSLKEYLOGFILE is used.") .help("Log TLS keys into PATH. If no path is specified the value of the envvar SSLKEYLOGFILE is used.")
.long("tls-key-log") .long("tls-key-log")
.value_name("PATH") .value_name("PATH")
.takes_value(true) .takes_value(true)
@ -119,9 +122,7 @@ fn main() -> miette::Result<()> {
Err(error) => error.exit(), Err(error) => error.exit(),
}; };
let configpath = matches let configpath = matches.value_of("config").unwrap_or("/etc/bffh/bffh.dhall");
.value_of("config")
.unwrap_or("/etc/difluoroborane.dhall");
// Check for the --print-default option first because we don't need to do anything else in that // Check for the --print-default option first because we don't need to do anything else in that
// case. // case.
@ -130,7 +131,7 @@ fn main() -> miette::Result<()> {
let encoded = serde_dhall::serialize(&config).to_string().unwrap(); let encoded = serde_dhall::serialize(&config).to_string().unwrap();
// Direct writing to fd 1 is faster but also prevents any print-formatting that could // Direct writing to fd 1 is faster but also prevents any print-formatting that could
// invalidate the generated TOML // invalidate the generated DHALL
let stdout = io::stdout(); let stdout = io::stdout();
let mut handle = stdout.lock(); let mut handle = stdout.lock();
handle.write_all(encoded.as_bytes()).unwrap(); handle.write_all(encoded.as_bytes()).unwrap();
@ -184,9 +185,13 @@ fn main() -> miette::Result<()> {
} else if matches.is_present("load-users") { } else if matches.is_present("load-users") {
let bffh = Difluoroborane::new(config)?; let bffh = Difluoroborane::new(config)?;
bffh.users.load_file(matches.value_of("load-users").unwrap())?; bffh.users
.load_file(matches.value_of("load-users").unwrap())?;
tracing::info!("loaded users from {}", matches.value_of("load-users").unwrap()); tracing::info!(
"loaded users from {}",
matches.value_of("load-users").unwrap()
);
return Ok(()); return Ok(());
} else { } else {