Quick and Dirty, in Rust?
So, I had a problem yesterday, just needed to execute a few LDAP queries to get the managers of a list of folk’s login IDs. Seems pretty simple, and I’ve done a bunch of LDAP “stuff” before so seemed like a simple enough job. Well, my first choice for simple jobs is to turn to Racket but in this case the LDAP support wasn’t what I really wanted. Next choice is a shell (bash or zsh) script using ldapsearch
, right?
All, I need to do is take a bunch of user IDs, call ldapsearch
which returns results in a regular “key: value\n
” with a line for each of the attributes listed on the command line.
$ ldapsearch -LLL -x -h $LDAP_HOST -p $LDAP_PORT -b $LDAP_BIND \
"(uid=$uid)" \
uid description manager jobfamily joblevel
This is easy enough to accomplish in a loop, but (and there’s always at least one) I need to combine each result into a single CSV (comma-separated value) file with one line per UID. Now I can start messing with cut
, tr
, or maybe sed
but by now this effort was no longer quick, but was definitely getting dirty. OK, so this wasn’t working so what next, Python maybe? It does have great libraries and a reputation for ease of use, and I’ve done it plenty of times before. Or, which is a wild idea, Rust? The language that often gets bashed for being complicated (borrow checker, etc.) and slow (coooommmmmmpillller) to develop in.
First step as always:
$ cargo init --bin --edition 2021 ldapq
Created binary (application) package
Now we have to do a quick check on crate.io, or alternatively use the crates-io-cli tool, for an LDAP package. I like the look of ldap3
, so we add it to our dependencies.
[dependencies]
ldap3="0.9"
I’ve used the csv
crate before, and I usually use structopt
for command-line parsing, so we end up with:
[dependencies]
csv = "1.1"
ldap3="0.9"
structopt = "0.3"
Step #1, we create a struct
to model our command-line:
use structopt::StructOpt;
#[derive(Clone, Debug, StructOpt)]
struct CommandLine {
#[structopt(short = "c", long)]
connection: String,
#[structopt(short = "f", long = "from")]
query_from: String,
#[structopt(short = "w", long = "where")]
query_where: Vec<String>,
#[structopt(short = "p", long = "project")]
query_project: Vec<String>,
}
Step #2, the main
function will create an LDAP connection, execute the query and pass the results to the function format_as_csv
.
use ldap3::{LdapConn, Scope};
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut options = CommandLine::from_args();
let mut connection = LdapConn::new(&options.connection)?;
let query_where = format!(
"(|{})",
options
.query_where
.iter()
.map(|v| format!("({})", v))
.collect::<Vec<String>>()
.join("")
);
if options.query_project.is_empty() {
options.query_project = vec![
"uid".to_string(),
"description".to_string(),
"manager".to_string(),
"jobfamily".to_string(),
"joblevel".to_string(),
];
}
let (rs, _res) = connection
.search(
&options.query_from,
Scope::Subtree,
&query_where,
options.query_project.clone(),
)?
.success()?;
format_as_csv(options.query_project, rs)?;
Ok(connection.unbind()?)
}
Step #3, read each ResultEntry
, convert into a SearchEntry
and then into a CSV row using a csv::Writer
.
use ldap3::{ResultEntry, SearchEntry};
fn format_as_csv(headers: Vec<String>, rows: Vec<ResultEntry>) -> Result<(), Box<dyn Error>> {
use csv::WriterBuilder;
let mut writer = WriterBuilder::new()
.has_headers(true)
.double_quote(true)
.flexible(false)
.from_writer(std::io::stdout());
let record_length = headers.len();
writer.write_record(headers.clone())?;
for row in rows {
let in_row = SearchEntry::construct(row);
let mut out_row: Vec<String> = Vec::with_capacity(record_length);
for (i, key) in headers.iter().enumerate() {
let value = match in_row.attrs.get(key) {
None => String::new(),
Some(values) => values.join(COMMA),
};
out_row.insert(i, value);
}
writer.write_record(out_row)?;
}
writer.flush()?;
Ok(())
}
It really was quick, not so very dirty, and honestly easy to extend (I added JSON output using the serde_json
crate). Was it easier than a shell script? Yes, given the specific complexity the script result would have been pretty much write-only whereas the Rust code is eminently readable.