From f1b15332689690358fc22ce46f29bd214b9c99fe Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Sun, 14 Jul 2024 15:55:05 -0500 Subject: [PATCH] Adding admin commands --- src/cli/admin.rs | 131 +++++++++++++++++++++++++++++++++++++++-------- src/db.rs | 11 ++++ 2 files changed, 121 insertions(+), 21 deletions(-) diff --git a/src/cli/admin.rs b/src/cli/admin.rs index 78c7fb2..ab8bb96 100644 --- a/src/cli/admin.rs +++ b/src/cli/admin.rs @@ -1,15 +1,32 @@ +use color_eyre::eyre::anyhow; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use sled::IVec; +use uuid::Uuid; + +use crate::{ + db::{self, Data}, + user::{self, User}, +}; + use super::prelude::*; #[derive(Args)] pub struct Admin { - /// Register a user account #[command(subcommand)] - command: Commands, + command: AdminCommands, } #[derive(Subcommand)] -enum Commands { - CreateAccount(CreateAccount), +pub enum AdminCommands { + #[command(subcommand)] + Accounts(AccountsCommands), +} + +#[derive(Subcommand)] +enum AccountsCommands { + Create(Create), + Delete(Delete), + List(List), } #[derive(Error, Debug)] @@ -24,40 +41,112 @@ pub enum RunError { impl Admin { pub async fn run(&self) -> Result<(), RunError> { match &self.command { - Commands::CreateAccount(args) => Ok(args.run().await?), + AdminCommands::Accounts(accounts) => match accounts { + AccountsCommands::Create(args) => Ok(args.run().await?), + AccountsCommands::Delete(args) => Ok(args.run().await?), + AccountsCommands::List(args) => Ok(args.run().await?), + }, } } } +#[derive(Args)] +pub struct List {} +impl List { + pub async fn run(&self) -> Result<(), CreateAccountError> { + let db = Data::try_new()?; + for entry in db.all::(User::tree())? { + if let Ok((_, user)) = entry { + println!("ID: {}, Username: {}", user.id, user.username); + } else { + println!("Invalid user entry when iterating database: {:?}", entry) + } + } + Ok(()) + } +} + +#[derive(Args)] +pub struct Delete { + id: Uuid, +} +impl Delete { + pub async fn run(&self) -> Result<(), color_eyre::Report> { + let db = Data::try_new()?; + let result = db.delete::(User::tree(), self.id)?; + if let Some(user) = result { + println!( + "Deleted user with username: {} (ID: {}", + user.username, self.id + ); + } else { + return Err(anyhow!("user not found")); + } + Ok(()) + } +} + /// Create a user account #[derive(Args)] -pub struct CreateAccount { +pub struct Create { /// Whether or not the user is a site super admin - #[arg(short, long, default_value = "false")] + #[arg(long, default_value = "false")] pub superadmin: bool, - /// The email address of the user account - #[arg(short = 'e', long)] - pub email_address: String, + /// The email address of the account + // #[arg(short = 'e', long)] + // pub email_address: String, - /// The user's initial password - if none is set, a random one will be used and output - #[arg(short, long)] + /// The username of the new account + #[arg(short = 'u', long)] + pub username: String, + + /// The account's initial password - if none is set, a random one will be used and output + #[arg(short = 'p', long)] pub initial_password: Option, } #[derive(Error, Debug)] -pub enum CreateAccountError {} +pub enum CreateAccountError { + #[error("password hash error: {0}")] + PasswordHash(#[from] argon2::password_hash::Error), -impl CreateAccount { + #[error("data error: {0}")] + Data(#[from] db::Error), + + #[error("user error: {0}")] + User(#[from] user::Error), + + #[error("bincode error: {0}")] + Bincode(#[from] bincode::Error), +} + +impl Create { pub async fn run(&self) -> Result<(), CreateAccountError> { // self.email_address - let password = self.initial_password.unwrap_or_else(|| { - rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(7) - .map(char::from) - .collect() - }); + let password: String = if let Some(password) = self.initial_password.as_ref() { + password.clone() + } else { + let generated_password: String = (0..16) + .map(|_| thread_rng().sample(Alphanumeric) as char) + .collect(); + println!("Generated password: {generated_password}"); + generated_password + }; + + let user = User::try_new(&self.username, &password)?; + let db = Data::try_new()?; + let existing_user: Option = db.get(User::tree(), &self.username)?; + + // TODO: fail2ban? + if existing_user.is_some() { + // timing/enumeration attacks or something + return Err(user::Error::UsernameExists(Box::new(self.username.clone())).into()); + } + + db.insert(User::tree(), &user.username, bincode::serialize(&user)?)?; + + println!("Created user with username: {}", self.username); Ok(()) } diff --git a/src/db.rs b/src/db.rs index ac97be5..b852968 100644 --- a/src/db.rs +++ b/src/db.rs @@ -23,6 +23,17 @@ impl Data { }) } + pub fn delete(&self, tree_name: &str, key: K) -> Result, Error> + where + K: AsRef<[u8]>, + V: DeserializeOwned, + { + match self.db.open_tree(tree_name)?.remove(key.as_ref())? { + Some(v) => Ok(Some(bincode::deserialize::(&v)?)), + None => Ok(None), + } + } + pub fn get(&self, tree_name: &str, key: K) -> Result, Error> where K: AsRef<[u8]>,