diff --git a/Cargo.toml b/Cargo.toml index ec8517b..7d7080d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ ldap3 = "0.12.1" serde = { version = "1.0.229", features = ["derive"] } tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] } toml = "1.1.6" +clap_mangen = "0.3.3" \ No newline at end of file diff --git a/crates/ldap/src/client.rs b/crates/ldap/src/client.rs index 8b13789..64fb539 100644 --- a/crates/ldap/src/client.rs +++ b/crates/ldap/src/client.rs @@ -1 +1,39 @@ +use anyhow::Result; +use ldap3::{LdapConn, Scope, SearchEntry}; +use config::ConnectionConfig; + +/// LDAP client. +pub struct LdapClient { + connection: LdapConn, +} + +impl LdapClient { + /// Connect to the LDAP server using the read credentials. + pub fn connect(config: &ConnectionConfig) -> Result { + let mut connection = LdapConn::new(&config.ldap.url)?; + + connection + .simple_bind(&config.read.bind_dn, &config.read.bind_pw)? + .success()?; + + Ok(Self { connection }) + } + + /// Search the LDAP directory. + /// + /// Returns the matching entries as `ldap3::SearchEntry` values. + pub fn search( + &mut self, + base_dn: &str, + filter: &str, + attributes: Vec<&str>, + ) -> Result> { + let (entries, _) = self + .connection + .search(base_dn, Scope::Subtree, filter, attributes)? + .success()?; + + Ok(entries.into_iter().map(SearchEntry::construct).collect()) + } +} diff --git a/crates/ldap/src/lib.rs b/crates/ldap/src/lib.rs index 7a31f60..3187d1f 100644 --- a/crates/ldap/src/lib.rs +++ b/crates/ldap/src/lib.rs @@ -1,5 +1,3 @@ mod client; -mod user; -//pub use client::LdapClient; -pub use user::User; +pub use client::LdapClient; diff --git a/tools/lk-search/Cargo.toml b/tools/lk-search/Cargo.toml index 26dc4d3..85624ca 100644 --- a/tools/lk-search/Cargo.toml +++ b/tools/lk-search/Cargo.toml @@ -9,4 +9,9 @@ repository.workspace = true anyhow.workspace = true clap.workspace = true config = { path = "../../crates/config" } +ldap = { path = "../../crates/ldap" } ldap3.workspace = true + +[build-dependencies] +clap.workspace = true +clap_mangen.workspace = true \ No newline at end of file diff --git a/tools/lk-search/build.rs b/tools/lk-search/build.rs new file mode 100644 index 0000000..f19e524 --- /dev/null +++ b/tools/lk-search/build.rs @@ -0,0 +1,23 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +use clap::CommandFactory; +use clap_mangen::Man; + +#[path = "src/args.rs"] +mod args; + +fn main() -> std::io::Result<()> { + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + + let command = args::Args::command(); + let man = Man::new(command); + + let mut output = Vec::new(); + man.render(&mut output)?; + + fs::write(out_dir.join("lk-search.1"), output)?; + + Ok(()) +} diff --git a/tools/lk-search/src/args.rs b/tools/lk-search/src/args.rs new file mode 100644 index 0000000..f220ae2 --- /dev/null +++ b/tools/lk-search/src/args.rs @@ -0,0 +1,18 @@ +use clap::{ArgGroup, Parser}; + +#[derive(Debug, Parser)] +#[command(name = "lk-search", about = "Search an LDAP directory")] +#[command(group( + ArgGroup::new("search") + .required(true) + .args(["username", "id"]) +))] +pub struct Args { + /// Search by username. + #[arg(short = 'u', long = "username", value_name = "USERNAME")] + pub username: Option, + + /// Search by identifier. + #[arg(short = 'i', long = "id", value_name = "ID")] + pub id: Option, +} diff --git a/tools/lk-search/src/main.rs b/tools/lk-search/src/main.rs index 0724b1f..2744de5 100644 --- a/tools/lk-search/src/main.rs +++ b/tools/lk-search/src/main.rs @@ -1,49 +1,31 @@ +mod args; + use anyhow::{Result, bail}; -use clap::{ArgGroup, Parser}; -use ldap3::{LdapConn, Scope, SearchEntry}; +use clap::Parser; use config::ConnectionConfig; +use ldap::LdapClient; -#[derive(Debug, Parser)] -#[command(name = "lk-search", about = "Search an LDAP user")] -#[command(group( - ArgGroup::new("user") - .required(true) - .args(["username", "id_number"]) -))] -struct Args { - /// Search by username. - #[arg(short = 'u', long = "username", value_name = "USERNAME")] - username: Option, - - /// Search by ID number. - #[arg(short = 'n', long = "id-number", value_name = "ID")] - id_number: Option, -} +use args::Args; fn main() -> Result<()> { let args = Args::parse(); let config = ConnectionConfig::load()?; - let (search_attribute, search_value) = match (&args.username, &args.id_number) { + let (search_attribute, search_value) = match (&args.username, &args.id) { (Some(username), None) => ("uid", username), - (None, Some(id_number)) => ("uidNumber", id_number), + (None, Some(id)) => ("uidNumber", id), _ => unreachable!(), }; let filter = format!("({search_attribute}={search_value})"); - let mut ldap = LdapConn::new(&config.ldap.url)?; + let mut ldap = LdapClient::connect(&config)?; - ldap.simple_bind(&config.read.bind_dn, &config.read.bind_pw)? - .success()?; - - let (entries, _result) = ldap - .search(&config.ldap.base_dn, Scope::Subtree, &filter, vec!["*"])? - .success()?; + let entries = ldap.search(&config.ldap.base_dn, &filter, vec!["*"])?; if entries.is_empty() { - bail!("User not found."); + bail!("No matching entries found."); } if entries.len() > 1 { @@ -53,11 +35,11 @@ fn main() -> Result<()> { ); } - let entry = SearchEntry::construct(entries.into_iter().next().unwrap()); + let entry = &entries[0]; println!("DN: {}", entry.dn); - for (attribute, values) in entry.attrs { + for (attribute, values) in &entry.attrs { println!("{attribute}:"); for value in values {