From b4d7ada317699465effcb3ad38563e5caa5d1309 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 20 Mar 2023 23:35:50 +0100 Subject: [PATCH] lldap_set_password: create the new tool Fixes #473. --- Cargo.lock | 13 ++++ Cargo.toml | 3 +- set-password/Cargo.toml | 25 ++++++++ set-password/src/main.rs | 133 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 set-password/Cargo.toml create mode 100644 set-password/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 98477c3..4fbc339 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2456,6 +2456,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "lldap_set_password" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "lldap_auth", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", +] + [[package]] name = "local-channel" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 2c6830c..4cf17a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ members = [ "server", "auth", "app", - "migration-tool" + "migration-tool", + "set-password", ] default-members = ["server"] diff --git a/set-password/Cargo.toml b/set-password/Cargo.toml new file mode 100644 index 0000000..e35e8b0 --- /dev/null +++ b/set-password/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "lldap_set_password" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "*" +rand = "0.8" +serde = "1" +serde_json = "1" + +[dependencies.clap] +features = ["std", "color", "suggestions", "derive", "env"] +version = "4" + +[dependencies.lldap_auth] +path = "../auth" +features = ["opaque_client"] + +[dependencies.reqwest] +version = "*" +default-features = false +features = ["json", "blocking", "rustls-tls"] diff --git a/set-password/src/main.rs b/set-password/src/main.rs new file mode 100644 index 0000000..5cbf184 --- /dev/null +++ b/set-password/src/main.rs @@ -0,0 +1,133 @@ +use anyhow::{bail, ensure, Context, Result}; +use clap::Parser; +use lldap_auth::{opaque, registration}; +use serde::Serialize; + +/// Set the password for a user in LLDAP. +#[derive(Debug, Parser, Clone)] +pub struct CliOpts { + /// Base LLDAP url, e.g. "https://lldap/". + #[clap(short, long)] + pub base_url: String, + + /// Admin username. + #[clap(long, default_value = "admin")] + pub admin_username: String, + + /// Admin password. + #[clap(long)] + pub admin_password: Option, + + /// Connection token (JWT). + #[clap(short, long)] + pub token: Option, + + /// Username. + #[clap(short, long)] + pub username: String, + + /// New password for the user. + #[clap(short, long)] + pub password: String, +} + +fn get_token(base_url: &str, username: &str, password: &str) -> Result { + let client = reqwest::blocking::Client::new(); + let response = client + .post(format!("{base_url}/auth/simple/login")) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body( + serde_json::to_string(&lldap_auth::login::ClientSimpleLoginRequest { + username: username.to_string(), + password: password.to_string(), + }) + .expect("Failed to encode the username/password as json to log in"), + ) + .send()? + .error_for_status()?; + Ok(serde_json::from_str::(&response.text()?)?.token) +} + +fn call_server(url: &str, token: &str, body: impl Serialize) -> Result { + let client = reqwest::blocking::Client::new(); + let request = client + .post(url) + .header("Content-Type", "application/json") + .bearer_auth(token) + .body(serde_json::to_string(&body)?); + let response = request.send()?.error_for_status()?; + Ok(response.text()?) +} + +pub fn register_start( + base_url: &str, + token: &str, + request: registration::ClientRegistrationStartRequest, +) -> Result { + let request = Some(request); + let data = call_server( + &format!("{base_url}/auth/opaque/register/start"), + token, + request, + )?; + serde_json::from_str(&data).context("Could not parse response") +} + +pub fn register_finish( + base_url: &str, + token: &str, + request: registration::ClientRegistrationFinishRequest, +) -> Result<()> { + let request = Some(request); + call_server( + &format!("{base_url}/auth/opaque/register/finish"), + token, + request, + ) + .map(|_| ()) +} + +fn main() -> Result<()> { + let opts = CliOpts::parse(); + ensure!( + opts.password.len() >= 8, + "New password is too short, expected at least 8 characters" + ); + ensure!( + opts.base_url.starts_with("http://") || opts.base_url.starts_with("https://"), + "Base URL should start with `http://` or `https://`" + ); + let token = match (opts.token.as_ref(), opts.admin_password.as_ref()) { + (Some(token), _) => token.clone(), + (None, Some(password)) => { + get_token(&opts.base_url, &opts.admin_username, password).context("While logging in")? + } + (None, None) => bail!("Either the token or the admin password is required"), + }; + + let mut rng = rand::rngs::OsRng; + let registration_start_request = + opaque::client::registration::start_registration(&opts.password, &mut rng) + .context("Could not initiate password change")?; + let start_request = registration::ClientRegistrationStartRequest { + username: opts.username.to_string(), + registration_start_request: registration_start_request.message, + }; + let res = register_start(&opts.base_url, &token, start_request)?; + + let registration_finish = opaque::client::registration::finish_registration( + registration_start_request.state, + res.registration_response, + &mut rng, + ) + .context("Error during password change")?; + let req = registration::ClientRegistrationFinishRequest { + server_data: res.server_data, + registration_upload: registration_finish.message, + }; + + register_finish(&opts.base_url, &token, req)?; + + println!("Successfully changed {}'s password", &opts.username); + Ok(()) +}