mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 11:42:25 +00:00
Add some CLI commands to generate a default configuration and password hashes
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -2393,6 +2393,17 @@ version = "1.9.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
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]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
@@ -2454,6 +2465,16 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "rust-embed"
|
name = "rust-embed"
|
||||||
version = "8.5.0"
|
version = "8.5.0"
|
||||||
@@ -2509,6 +2530,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"clap",
|
"clap",
|
||||||
"console-subscriber",
|
"console-subscriber",
|
||||||
@@ -2517,6 +2539,10 @@ dependencies = [
|
|||||||
"opentelemetry-otlp",
|
"opentelemetry-otlp",
|
||||||
"opentelemetry-semantic-conventions",
|
"opentelemetry-semantic-conventions",
|
||||||
"opentelemetry_sdk",
|
"opentelemetry_sdk",
|
||||||
|
"password-hash",
|
||||||
|
"pbkdf2",
|
||||||
|
"rand",
|
||||||
|
"rpassword",
|
||||||
"rustical_caldav",
|
"rustical_caldav",
|
||||||
"rustical_carddav",
|
"rustical_carddav",
|
||||||
"rustical_frontend",
|
"rustical_frontend",
|
||||||
|
|||||||
23
Cargo.toml
23
Cargo.toml
@@ -81,6 +81,10 @@ rustical_caldav = { path = "./crates/caldav/" }
|
|||||||
rustical_carddav = { path = "./crates/carddav/" }
|
rustical_carddav = { path = "./crates/carddav/" }
|
||||||
rustical_frontend = { path = "./crates/frontend/" }
|
rustical_frontend = { path = "./crates/frontend/" }
|
||||||
chrono-tz = "0.10.0"
|
chrono-tz = "0.10.0"
|
||||||
|
rand = "0.8"
|
||||||
|
argon2 = "0.5"
|
||||||
|
rpassword = "7.3"
|
||||||
|
password-hash = { version = "0.5" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_store = { workspace = true }
|
rustical_store = { workspace = true }
|
||||||
@@ -100,16 +104,21 @@ sqlx = { workspace = true }
|
|||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
tracing-actix-web = { workspace = true }
|
tracing-actix-web = { workspace = true }
|
||||||
|
|
||||||
# 0.25 is the latest version supported by tracing-opentelemetry
|
opentelemetry = "0.26"
|
||||||
opentelemetry = "0.26.0"
|
opentelemetry-otlp = "0.26"
|
||||||
opentelemetry-otlp = "0.26.0"
|
opentelemetry_sdk = { version = "0.26", features = ["rt-tokio"] }
|
||||||
opentelemetry_sdk = { version = "0.26.0", features = ["rt-tokio"] }
|
|
||||||
|
|
||||||
opentelemetry-semantic-conventions = "0.26"
|
opentelemetry-semantic-conventions = "0.26"
|
||||||
tracing-opentelemetry = "0.27.0"
|
tracing-opentelemetry = "0.27"
|
||||||
tracing-subscriber = { version = "0.3.18", features = [
|
tracing-subscriber = { version = "0.3", features = [
|
||||||
"env-filter",
|
"env-filter",
|
||||||
"fmt",
|
"fmt",
|
||||||
"registry",
|
"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
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct FrontendConfig {
|
pub struct FrontendConfig {
|
||||||
#[serde(serialize_with = "hex::serde::serialize")]
|
#[serde(serialize_with = "hex::serde::serialize")]
|
||||||
#[serde(deserialize_with = "hex::serde::deserialize")]
|
#[serde(deserialize_with = "hex::serde::deserialize")]
|
||||||
pub secret_key: Vec<u8>,
|
pub secret_key: [u8; 64],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use super::AuthenticationProvider;
|
use super::AuthenticationProvider;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
pub struct StaticUserStoreConfig {
|
pub struct StaticUserStoreConfig {
|
||||||
users: Vec<UserEntry>,
|
pub users: Vec<UserEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|||||||
94
src/commands/mod.rs
Normal file
94
src/commands/mod.rs
Normal 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(())
|
||||||
|
}
|
||||||
23
src/main.rs
23
src/main.rs
@@ -2,7 +2,8 @@ use crate::config::Config;
|
|||||||
use actix_web::HttpServer;
|
use actix_web::HttpServer;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use app::make_app;
|
use app::make_app;
|
||||||
use clap::Parser;
|
use clap::{Parser, Subcommand};
|
||||||
|
use commands::{cmd_gen_config, cmd_pwhash};
|
||||||
use config::{DataStoreConfig, SqliteDataStoreConfig};
|
use config::{DataStoreConfig, SqliteDataStoreConfig};
|
||||||
use rustical_store::auth::StaticUserStore;
|
use rustical_store::auth::StaticUserStore;
|
||||||
use rustical_store::{AddressbookStore, CalendarStore};
|
use rustical_store::{AddressbookStore, CalendarStore};
|
||||||
@@ -12,16 +13,26 @@ use std::fs;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod setup_tracing;
|
mod setup_tracing;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg(short, long, env)]
|
#[arg(short, long, env, default_value = "/etc/rustical/config.toml")]
|
||||||
config_file: String,
|
config_file: String,
|
||||||
#[arg(long, env, help = "Run database migrations (only for sql store)")]
|
#[arg(long, env, help = "Run database migrations (only for sql store)")]
|
||||||
migrate: bool,
|
migrate: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Option<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum Command {
|
||||||
|
GenConfig(commands::GenConfigArgs),
|
||||||
|
Pwhash(commands::PwhashArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_data_stores(
|
async fn get_data_stores(
|
||||||
@@ -40,6 +51,11 @@ async fn get_data_stores(
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
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 config: Config = toml::from_str(&fs::read_to_string(&args.config_file)?)?;
|
||||||
|
|
||||||
setup_tracing(&config.tracing);
|
setup_tracing(&config.tracing);
|
||||||
@@ -61,6 +77,7 @@ async fn main() -> Result<()> {
|
|||||||
.bind((config.http.host, config.http.port))?
|
.bind((config.http.host, config.http.port))?
|
||||||
.run()
|
.run()
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user