use rhai::{
plugin::{
mem, Dynamic, FnAccess, FnNamespace, ImmutableString, NativeCallContext, PluginFunction,
RhaiResult, TypeId,
},
Module,
};
use ldap3::{exop::WhoAmI, LdapConn, LdapError};
use r2d2::ManageConnection;
macro_rules! rhai_generic_ok {
($result:expr) => {
$result.map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())?
};
}
#[derive(Debug)]
pub struct ConnectionManager {
url: String,
tls: Option<LdapTLSParameters>,
bind: Option<LdapBindParameters>,
}
impl ManageConnection for ConnectionManager {
type Connection = LdapConn;
type Error = LdapError;
fn connect(&self) -> Result<LdapConn, LdapError> {
let mut conn = self.tls.as_ref().map_or_else(
|| LdapConn::new(&self.url),
|tls| {
let settings = ldap3::LdapConnSettings::new().set_starttls(tls.starttls);
let settings = if let Some(cafile) = tls.cafile.as_ref() {
let mut root_store = rustls::RootCertStore::empty();
let cert = std::fs::File::open(cafile)?;
let mut reader = std::io::BufReader::new(cert);
root_store.add_parsable_certificates(&rustls_pemfile::certs(&mut reader)?);
let config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
settings.set_config(config.into())
} else {
settings
};
LdapConn::with_settings(settings, &self.url)
},
)?;
if let Some(bind) = &self.bind {
conn.simple_bind(&bind.dn, &bind.pw)?.success()?;
}
Ok(conn)
}
fn is_valid(&self, conn: &mut LdapConn) -> Result<(), LdapError> {
conn.extended(WhoAmI).map(|_| ())
}
fn has_broken(&self, conn: &mut LdapConn) -> bool {
conn.extended(WhoAmI).is_err()
}
}
#[derive(Debug, serde::Deserialize)]
pub struct LdapBindParameters {
dn: String,
pw: String,
}
#[derive(serde::Deserialize)]
pub struct LdapParameters {
pub url: String,
#[serde(default = "default_timeout", with = "humantime_serde")]
pub timeout: std::time::Duration,
#[serde(default = "default_connections")]
pub connections: rhai::INT,
#[serde(default)]
pub tls: Option<LdapTLSParameters>,
#[serde(default)]
pub bind: Option<LdapBindParameters>,
}
const fn default_connections() -> rhai::INT {
4
}
const fn default_timeout() -> std::time::Duration {
std::time::Duration::from_secs(30)
}
#[derive(Debug, serde::Deserialize)]
pub struct LdapTLSParameters {
#[serde(default)]
starttls: bool,
#[serde(default)]
cafile: Option<std::path::PathBuf>,
}
#[derive(Clone)]
pub struct Ldap {
pub url: String,
pub pool: r2d2::Pool<ConnectionManager>,
}
impl Ldap {
pub fn with_parameters(parameters: LdapParameters) -> Result<Self, Box<rhai::EvalAltResult>> {
Ok(Self {
url: parameters.url.clone(),
pool: rhai_generic_ok!(r2d2::Pool::builder()
.max_size(rhai_generic_ok!(u32::try_from(parameters.connections)))
.connection_timeout(parameters.timeout)
.build(ConnectionManager {
url: parameters.url,
tls: parameters.tls,
bind: parameters.bind,
})),
})
}
pub fn get(&self) -> Result<r2d2::PooledConnection<ConnectionManager>, String> {
self.pool
.get()
.map_err(|error| format!("failed to get an ldap connection: {error}"))
}
pub fn search(
&self,
base: &str,
scope: &str,
filter: &str,
attrs: Vec<String>,
) -> Result<ldap3::SearchResult, ldap3::LdapError> {
let mut conn = self.get().map_err(|err| ldap3::LdapError::Io {
source: std::io::Error::new(std::io::ErrorKind::TimedOut, err),
})?;
conn.search(
base,
Self::ldap_scope_from_string(scope)
.map_err(|_| ldap3::LdapError::InvalidScopeString(scope.to_owned()))?,
filter,
attrs,
)
}
pub fn compare(&self, dn: &str, attr: &str, val: &str) -> Result<bool, String> {
let mut conn = self.get()?;
conn.compare(dn, attr, val)
.map_err::<String, _>(|error| {
format!("failed to execute ldap compare command: {error}")
})?
.equal()
.map_err::<String, _>(|error| {
format!("the ldap client returned an non true or false code: {error}")
})
}
fn ldap_scope_from_string(s: &str) -> Result<ldap3::Scope, String> {
match s {
"base" => Ok(ldap3::Scope::Base),
"one" => Ok(ldap3::Scope::OneLevel),
"sub" => Ok(ldap3::Scope::Subtree),
scope => Err(format!("'scope' parameter is malformed, it should either be 'base', 'one' or 'sub', not '{scope}'")),
}
}
fn rhai_result_from_ldap(entry: ldap3::ResultEntry) -> rhai::Map {
let entry = ldap3::SearchEntry::construct(entry);
rhai::Map::from_iter([
("dn".into(), rhai::Dynamic::from(entry.dn)),
(
"attrs".into(),
rhai::Dynamic::from_map(
entry
.attrs
.into_iter()
.map(|(key, value)| {
(
key.into(),
value.into_iter().map(rhai::Dynamic::from).collect(),
)
})
.collect::<rhai::Map>(),
),
),
])
}
}
#[rhai::plugin::export_module]
pub mod ldap {
pub type Ldap = rhai::Shared<super::Ldap>;
#[rhai_fn(return_raw)]
pub fn connect(parameters: rhai::Map) -> Result<Ldap, Box<rhai::EvalAltResult>> {
let parameters = rhai::serde::from_dynamic::<super::LdapParameters>(¶meters.into())?;
super::Ldap::with_parameters(parameters).map(rhai::Shared::new)
}
#[rhai_fn(global, return_raw, pure)]
pub fn search(
database: &mut Ldap,
base: &str,
scope: &str,
filter: &str,
attrs: rhai::Array,
) -> Result<rhai::Map, Box<rhai::EvalAltResult>> {
let results = rhai_generic_ok!(database.search(
base,
scope,
filter,
attrs
.into_iter()
.map(|item| item.to_string())
.collect::<Vec<_>>(),
));
Ok(results.success().map_or_else(
|error| {
rhai::Map::from_iter([
("result".into(), "error".into()),
("error".into(), error.to_string().into()),
])
},
|(entries, _)| {
rhai::Map::from_iter([
("result".into(), rhai::Dynamic::from("ok")),
(
"entries".into(),
rhai::Dynamic::from_array(
entries
.into_iter()
.map(|entry| {
rhai::Dynamic::from_map(super::Ldap::rhai_result_from_ldap(
entry,
))
})
.collect::<rhai::Array>(),
),
),
])
},
))
}
#[rhai_fn(global, return_raw, pure)]
pub fn compare(
database: &mut Ldap,
dn: &str,
attr: &str,
val: &str,
) -> Result<bool, Box<rhai::EvalAltResult>> {
database
.compare(dn, attr, val)
.map_err(std::convert::Into::into)
}
}