Skip to content

Commit cf67743

Browse files
committed
Add --password flag to users create command
Support setting a password directly via CLI for headless login. Uses PBKDF2-HMAC-SHA256 (100k iterations) matching the API's hash format.
1 parent e8c559e commit cf67743

3 files changed

Lines changed: 95 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "openworkers-cli"
3-
version = "0.3.4"
3+
version = "0.3.5"
44
edition = "2024"
55
license = "MIT"
66
description = "CLI for OpenWorkers - Self-hosted Cloudflare Workers runtime"
@@ -58,6 +58,8 @@ hex = "0.4"
5858
base64 = "0.22"
5959
zip = { version = "7", default-features = false, features = ["deflate"] }
6060
hmac = "0.12"
61+
pbkdf2 = { version = "0.12", features = ["hmac"] }
62+
rand = "0.9"
6163
mime_guess = "2"
6264
futures = "0.3"
6365
url = "2"

src/commands/users.rs

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
use crate::config::{AliasConfig, Config, ConfigError};
2+
use base64::Engine;
3+
use base64::engine::general_purpose::STANDARD as BASE64;
24
use clap::Subcommand;
35
use colored::Colorize;
6+
use pbkdf2::hmac::Hmac;
7+
use rand::RngCore;
8+
use sha2::Sha256;
49
use sqlx::postgres::PgPoolOptions;
510
use sqlx::{PgPool, Row};
611

@@ -23,6 +28,12 @@ pub enum UsersError {
2328

2429
#[error("User '{0}' already exists")]
2530
UserExists(String),
31+
32+
#[error("Passwords do not match")]
33+
PasswordMismatch,
34+
35+
#[error("Password error: {0}")]
36+
Password(String),
2637
}
2738

2839
#[derive(Subcommand)]
@@ -41,14 +52,19 @@ pub enum UsersCommand {
4152
/// Create a new user (bootstrap mode - no user required)
4253
#[command(after_help = "Examples:\n \
4354
ow local users create max\n \
44-
ow local users create max --system")]
55+
ow local users create max --system\n \
56+
ow local users create max --password")]
4557
Create {
4658
/// Username for the new user
4759
username: String,
4860

4961
/// Claim the system user (rename __system__ to this username)
5062
#[arg(long)]
5163
system: bool,
64+
65+
/// Set a password for the user (prompts interactively)
66+
#[arg(long)]
67+
password: bool,
5268
},
5369

5470
/// Delete a user
@@ -70,7 +86,11 @@ impl UsersCommand {
7086
match self {
7187
Self::List => cmd_list(&pool).await,
7288
Self::Get { username } => cmd_get(&pool, &username).await,
73-
Self::Create { username, system } => cmd_create(&pool, username, system).await,
89+
Self::Create {
90+
username,
91+
system,
92+
password,
93+
} => cmd_create(&pool, username, system, password).await,
7494
Self::Delete { username } => cmd_delete(&pool, &username).await,
7595
}
7696
}
@@ -171,7 +191,53 @@ async fn cmd_get(pool: &PgPool, username: &str) -> Result<(), UsersError> {
171191
Ok(())
172192
}
173193

174-
async fn cmd_create(pool: &PgPool, username: String, system: bool) -> Result<(), UsersError> {
194+
fn hash_password(password: &str) -> String {
195+
const ITERATIONS: u32 = 100_000;
196+
const SALT_LEN: usize = 16;
197+
const KEY_LEN: usize = 32;
198+
199+
let mut salt = [0u8; SALT_LEN];
200+
rand::rng().fill_bytes(&mut salt);
201+
202+
let mut hash = [0u8; KEY_LEN];
203+
pbkdf2::pbkdf2::<Hmac<Sha256>>(password.as_bytes(), &salt, ITERATIONS, &mut hash)
204+
.expect("HMAC can be initialized with any key length");
205+
206+
format!(
207+
"{}${}${}",
208+
ITERATIONS,
209+
BASE64.encode(salt),
210+
BASE64.encode(hash)
211+
)
212+
}
213+
214+
fn prompt_password() -> Result<String, UsersError> {
215+
let password = rpassword::prompt_password("Password: ")
216+
.map_err(|e| UsersError::Password(e.to_string()))?;
217+
218+
let confirm =
219+
rpassword::prompt_password("Confirm: ").map_err(|e| UsersError::Password(e.to_string()))?;
220+
221+
if password != confirm {
222+
return Err(UsersError::PasswordMismatch);
223+
}
224+
225+
Ok(password)
226+
}
227+
228+
async fn cmd_create(
229+
pool: &PgPool,
230+
username: String,
231+
system: bool,
232+
password: bool,
233+
) -> Result<(), UsersError> {
234+
let password_hash = if password {
235+
let pw = prompt_password()?;
236+
Some(hash_password(&pw))
237+
} else {
238+
None
239+
};
240+
175241
if system {
176242
// Rename the system user to claim it
177243
let result = sqlx::query(
@@ -224,6 +290,16 @@ async fn cmd_create(pool: &PgPool, username: String, system: bool) -> Result<(),
224290
);
225291
}
226292

293+
if let Some(hash) = password_hash {
294+
sqlx::query("UPDATE users SET password_hash = $1 WHERE username = $2")
295+
.bind(&hash)
296+
.bind(&username)
297+
.execute(pool)
298+
.await?;
299+
300+
println!("{} Password set.", "Password".green().bold());
301+
}
302+
227303
println!("\n{} Set this user as default with:", "Next:".cyan().bold());
228304
println!(
229305
" {}",

0 commit comments

Comments
 (0)