use std::path::PathBuf;
use abscissa_core::{status_err, Application, Command, Runnable, Shutdown};
use anyhow::{bail, Result};
use clap::{Args, Parser, Subcommand};
use crate::{htpasswd::Htpasswd, prelude::RUSTIC_SERVER_APP};
#[derive(Command, Debug, Parser)]
pub struct AuthCmd {
#[command(subcommand)]
command: Commands,
}
impl Runnable for AuthCmd {
fn run(&self) {
if let Err(err) = self.inner_run() {
status_err!("{}", err);
RUSTIC_SERVER_APP.shutdown(Shutdown::Crash);
}
}
}
#[derive(Subcommand, Debug)]
enum Commands {
Add(AddArg),
Update(AddArg),
Delete(DelArg),
List(PrintArg),
}
#[derive(Args, Debug)]
struct AddArg {
#[arg(short = 'f')]
pub config_path: PathBuf,
#[arg(short = 'u')]
user: String,
#[arg(short = 'p')]
password: String,
}
#[derive(Args, Debug)]
struct DelArg {
#[arg(short = 'f')]
pub config_path: PathBuf,
#[arg(short = 'u')]
user: String,
}
#[derive(Args, Debug)]
struct PrintArg {
#[arg(short = 'f')]
pub config_path: PathBuf,
}
impl AuthCmd {
pub fn inner_run(&self) -> Result<()> {
match &self.command {
Commands::Add(arg) => {
add(arg)?;
}
Commands::Update(arg) => {
update(arg)?;
}
Commands::Delete(arg) => {
delete(arg)?;
}
Commands::List(arg) => {
print(arg)?;
}
};
Ok(())
}
}
fn check(path: &PathBuf) -> Result<()> {
if path.exists() {
if !path.is_file() {
bail!(
"Error: Given path leads to a folder, not a file: {}",
path.to_string_lossy()
);
}
if let Err(err) = std::fs::OpenOptions::new()
.create(false)
.truncate(false)
.append(true)
.open(path)
{
bail!(
"No write access to the htpasswd file: {} due to {}",
path.to_string_lossy(),
err
);
};
} else {
if let Err(err) = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(path)
{
bail!(
"Failed to create empty server configuration file: {} due to {}",
&path.to_string_lossy(),
err
);
};
};
Ok(())
}
fn add(arg: &AddArg) -> Result<()> {
let ht_access_path = PathBuf::from(&arg.config_path);
check(&ht_access_path)?;
let mut ht_access = Htpasswd::from_file(&ht_access_path)?;
if ht_access.users().contains(&arg.user.to_string()) {
bail!(
"User '{}' exists; use update to change password. No changes were made.",
arg.user.as_str()
);
}
let _ = ht_access.update(arg.user.as_str(), arg.password.as_str());
ht_access.to_file()?;
Ok(())
}
fn update(arg: &AddArg) -> Result<()> {
let ht_access_path = PathBuf::from(&arg.config_path);
check(&ht_access_path)?;
let mut ht_access = Htpasswd::from_file(&ht_access_path)?;
if !ht_access.credentials.contains_key(arg.user.as_str()) {
bail!(
"I can not find a user with name {}. Use add command?",
arg.user.as_str()
);
}
let _ = ht_access.update(arg.user.as_str(), arg.password.as_str());
ht_access.to_file()?;
Ok(())
}
fn delete(arg: &DelArg) -> Result<()> {
let ht_access_path = PathBuf::from(&arg.config_path);
check(&ht_access_path)?;
let mut ht_access = Htpasswd::from_file(&ht_access_path)?;
if ht_access.users().contains(&arg.user.to_string()) {
println!("Deleting user with name {}.", arg.user.as_str());
let _ = ht_access.delete(arg.user.as_str());
ht_access.to_file()?;
} else {
println!(
"Could not find a user with name {}. No changes were made.",
arg.user.as_str()
);
};
Ok(())
}
fn print(arg: &PrintArg) -> Result<()> {
let ht_access_path = PathBuf::from(&arg.config_path);
check(&ht_access_path)?;
let ht_access = Htpasswd::from_file(&ht_access_path)?;
println!("Listing users in the access file for a rustic_server.");
println!(
"\tConfiguration file used: {} ",
ht_access_path.to_string_lossy()
);
println!("List:");
for u in ht_access.users() {
println!("\t{}", u);
}
println!("Done.");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_auth() {
AuthCmd::command().debug_assert();
}
}