Add some CLI commands to generate a default configuration and password hashes

This commit is contained in:
Lennart
2024-11-03 13:17:38 +01:00
parent 3ea004f75d
commit 0f2db05a07
6 changed files with 175 additions and 29 deletions

26
Cargo.lock generated
View File

@@ -2393,6 +2393,17 @@ version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "rpassword"
version = "7.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
dependencies = [
"libc",
"rtoolbox",
"windows-sys 0.48.0",
]
[[package]]
name = "rsa"
version = "0.9.6"
@@ -2454,6 +2465,16 @@ dependencies = [
"syn",
]
[[package]]
name = "rtoolbox"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "rust-embed"
version = "8.5.0"
@@ -2509,6 +2530,7 @@ version = "0.1.0"
dependencies = [
"actix-web",
"anyhow",
"argon2",
"async-trait",
"clap",
"console-subscriber",
@@ -2517,6 +2539,10 @@ dependencies = [
"opentelemetry-otlp",
"opentelemetry-semantic-conventions",
"opentelemetry_sdk",
"password-hash",
"pbkdf2",
"rand",
"rpassword",
"rustical_caldav",
"rustical_carddav",
"rustical_frontend",

View File

@@ -81,6 +81,10 @@ rustical_caldav = { path = "./crates/caldav/" }
rustical_carddav = { path = "./crates/carddav/" }
rustical_frontend = { path = "./crates/frontend/" }
chrono-tz = "0.10.0"
rand = "0.8"
argon2 = "0.5"
rpassword = "7.3"
password-hash = { version = "0.5" }
[dependencies]
rustical_store = { workspace = true }
@@ -100,16 +104,21 @@ sqlx = { workspace = true }
async-trait = { workspace = true }
tracing-actix-web = { workspace = true }
# 0.25 is the latest version supported by tracing-opentelemetry
opentelemetry = "0.26.0"
opentelemetry-otlp = "0.26.0"
opentelemetry_sdk = { version = "0.26.0", features = ["rt-tokio"] }
opentelemetry = "0.26"
opentelemetry-otlp = "0.26"
opentelemetry_sdk = { version = "0.26", features = ["rt-tokio"] }
opentelemetry-semantic-conventions = "0.26"
tracing-opentelemetry = "0.27.0"
tracing-subscriber = { version = "0.3.18", features = [
tracing-opentelemetry = "0.27"
tracing-subscriber = { version = "0.3", features = [
"env-filter",
"fmt",
"registry",
] }
console-subscriber = "0.4.0"
console-subscriber = "0.4"
rand.workspace = true
rpassword.workspace = true
argon2.workspace = true
pbkdf2.workspace = true
password-hash.workspace = true

View File

@@ -5,5 +5,5 @@ use serde::{Deserialize, Serialize};
pub struct FrontendConfig {
#[serde(serialize_with = "hex::serde::serialize")]
#[serde(deserialize_with = "hex::serde::deserialize")]
pub secret_key: Vec<u8>,
pub secret_key: [u8; 64],
}

View File

@@ -5,9 +5,9 @@ use std::collections::HashMap;
use super::AuthenticationProvider;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct StaticUserStoreConfig {
users: Vec<UserEntry>,
pub users: Vec<UserEntry>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]

94
src/commands/mod.rs Normal file
View File

@@ -0,0 +1,94 @@
use argon2::password_hash::SaltString;
use clap::{Parser, ValueEnum};
use password_hash::PasswordHasher;
use pbkdf2::Params;
use rand::{rngs::OsRng, RngCore};
use rustical_frontend::FrontendConfig;
use rustical_store::auth::{static_user_store::UserEntry, StaticUserStoreConfig, User};
use crate::config::{
AuthConfig, Config, DataStoreConfig, HttpConfig, SqliteDataStoreConfig, TracingConfig,
};
#[derive(Debug, Parser)]
pub struct GenConfigArgs {}
fn generate_frontend_secret() -> [u8; 64] {
let mut rng = rand::thread_rng();
let mut secret = [0u8; 64];
rng.fill_bytes(&mut secret);
secret
}
pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
let config = Config {
http: HttpConfig::default(),
auth: AuthConfig::Static(StaticUserStoreConfig {
users: vec![UserEntry {
user: User {
id: "default".to_owned(),
displayname: Some("Default user".to_owned()),
password: Some("generate a password hash with rustical pwhash".to_owned()),
},
app_tokens: vec![],
}],
}),
data_store: DataStoreConfig::Sqlite(SqliteDataStoreConfig {
db_url: "".to_owned(),
}),
tracing: TracingConfig::default(),
frontend: FrontendConfig {
secret_key: generate_frontend_secret(),
},
};
let generated_config = toml::to_string(&config)?;
println!("{generated_config}");
Ok(())
}
#[derive(Debug, Clone, ValueEnum)]
enum PwhashAlgorithm {
#[value(help = "Use this for your password")]
Argon2,
#[value(help = "Significantly faster algorithm, use for app tokens")]
Pbkdf2,
}
#[derive(Debug, Parser)]
pub struct PwhashArgs {
#[arg(long, short = 'a')]
algorithm: PwhashAlgorithm,
#[arg(
long,
short = 'r',
help = "ONLY for pbkdf2: Number of rounds to calculate",
default_value_t = 1000
)]
rounds: u32,
}
pub fn cmd_pwhash(args: PwhashArgs) -> anyhow::Result<()> {
println!("Enter your password:");
let password = rpassword::read_password()?;
let salt = SaltString::generate(OsRng);
let password_hash = match args.algorithm {
PwhashAlgorithm::Argon2 => argon2::Argon2::default()
.hash_password(password.as_bytes(), &salt)
.unwrap(),
PwhashAlgorithm::Pbkdf2 => pbkdf2::Pbkdf2
.hash_password_customized(
password.as_bytes(),
None,
None,
Params {
rounds: args.rounds,
..Default::default()
},
&salt,
)
.unwrap(),
};
println!("{password_hash}");
Ok(())
}

View File

@@ -2,7 +2,8 @@ use crate::config::Config;
use actix_web::HttpServer;
use anyhow::Result;
use app::make_app;
use clap::Parser;
use clap::{Parser, Subcommand};
use commands::{cmd_gen_config, cmd_pwhash};
use config::{DataStoreConfig, SqliteDataStoreConfig};
use rustical_store::auth::StaticUserStore;
use rustical_store::{AddressbookStore, CalendarStore};
@@ -12,16 +13,26 @@ use std::fs;
use std::sync::Arc;
mod app;
mod commands;
mod config;
mod setup_tracing;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long, env)]
#[arg(short, long, env, default_value = "/etc/rustical/config.toml")]
config_file: String,
#[arg(long, env, help = "Run database migrations (only for sql store)")]
migrate: bool,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, Subcommand)]
enum Command {
GenConfig(commands::GenConfigArgs),
Pwhash(commands::PwhashArgs),
}
async fn get_data_stores(
@@ -40,27 +51,33 @@ async fn get_data_stores(
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let config: Config = toml::from_str(&fs::read_to_string(&args.config_file)?)?;
setup_tracing(&config.tracing);
match args.command {
Some(Command::GenConfig(gen_config_args)) => cmd_gen_config(gen_config_args)?,
Some(Command::Pwhash(pwhash_args)) => cmd_pwhash(pwhash_args)?,
None => {
let config: Config = toml::from_str(&fs::read_to_string(&args.config_file)?)?;
let (addr_store, cal_store) = get_data_stores(args.migrate, &config.data_store).await?;
setup_tracing(&config.tracing);
let user_store = Arc::new(match config.auth {
config::AuthConfig::Static(config) => StaticUserStore::new(config),
});
let (addr_store, cal_store) = get_data_stores(args.migrate, &config.data_store).await?;
HttpServer::new(move || {
make_app(
addr_store.clone(),
cal_store.clone(),
user_store.clone(),
config.frontend.clone(),
)
})
.bind((config.http.host, config.http.port))?
.run()
.await?;
let user_store = Arc::new(match config.auth {
config::AuthConfig::Static(config) => StaticUserStore::new(config),
});
HttpServer::new(move || {
make_app(
addr_store.clone(),
cal_store.clone(),
user_store.clone(),
config.frontend.clone(),
)
})
.bind((config.http.host, config.http.port))?
.run()
.await?;
}
}
Ok(())
}