use crate::error::VeltrixError;
use std::time::Instant;
use super::spec::{
LdapAuthMethod, LdapBackendUsed, LdapEmptyResponse, LdapResponse, LdapSpec, ServerType,
};
use super::types::{LdapEntry, ModifyOp, SearchOptions, SearchScope};
#[derive(Debug)]
pub struct LdapClient {
spec: LdapSpec,
backend_used: LdapBackendUsed,
connected: bool,
#[allow(dead_code)]
connection: Option<()>,
}
impl LdapClient {
pub fn new(spec: LdapSpec) -> Result<Self, VeltrixError> {
let server_type = ServerType::Unknown;
let backend_used = LdapBackendUsed {
server_type,
tls_mode_used: spec.tls_mode,
auth_method_used: spec.auth.to_string(),
connection_time_ms: 0,
};
Ok(Self {
spec,
backend_used,
connected: false,
connection: None,
})
}
pub fn spec(&self) -> &LdapSpec {
&self.spec
}
pub fn is_connected(&self) -> bool {
self.connected
}
pub fn bind(&mut self) -> Result<LdapResponse<()>, VeltrixError> {
let start = Instant::now();
let auth = self.spec.auth.clone();
match auth {
LdapAuthMethod::Simple { bind_dn, password } => {
self._bind_simple(&bind_dn, &password)?;
}
#[cfg(feature = "ldap-sasl")]
LdapAuthMethod::SaslPlain { identity, password } => {
self._bind_sasl_plain(&identity, &password)?;
}
#[cfg(feature = "ldap-sasl")]
LdapAuthMethod::SaslExternal => {
self._bind_sasl_external()?;
}
LdapAuthMethod::Anonymous => {
self._bind_anonymous()?;
}
}
self.backend_used.connection_time_ms = start.elapsed().as_millis() as u64;
self.connected = true;
Ok(LdapResponse::new((), self.backend_used.clone()))
}
pub fn bind_as(
&mut self,
user_dn: &str,
password: &str,
) -> Result<LdapResponse<()>, VeltrixError> {
let start = Instant::now();
if password.is_empty() {
return Err(VeltrixError::validation(
"password",
"password cannot be empty",
));
}
if user_dn.is_empty() {
return Err(VeltrixError::validation(
"user_dn",
"user_dn cannot be empty",
));
}
self._bind_simple(user_dn, password)?;
self.backend_used.connection_time_ms = start.elapsed().as_millis() as u64;
Ok(LdapResponse::new((), self.backend_used.clone()))
}
pub fn search(
&mut self,
options: SearchOptions,
) -> Result<LdapResponse<Vec<LdapEntry>>, VeltrixError> {
let start = Instant::now();
if !options.filter.starts_with('(') || !options.filter.ends_with(')') {
return Err(VeltrixError::validation(
"filter",
"filter must be enclosed in parentheses",
));
}
if options.base_dn.is_empty() {
return Err(VeltrixError::validation(
"base_dn",
"base_dn cannot be empty",
));
}
let entries = self._search_impl(&options)?;
self.backend_used.connection_time_ms = start.elapsed().as_millis() as u64;
Ok(LdapResponse::new(entries, self.backend_used.clone()))
}
pub fn get_entry(&mut self, dn: &str) -> Result<LdapResponse<Option<LdapEntry>>, VeltrixError> {
let start = Instant::now();
if dn.is_empty() {
return Err(VeltrixError::validation("dn", "DN cannot be empty"));
}
let mut options = SearchOptions::new(dn.to_string(), "(objectClass=*)".to_string());
options = options.with_scope(SearchScope::Base);
let result = self.search(options)?;
let entry = result.data.first().cloned();
self.backend_used.connection_time_ms = start.elapsed().as_millis() as u64;
Ok(LdapResponse::new(entry, result.backend_used))
}
pub fn find_user_by_uid(
&mut self,
uid: &str,
) -> Result<LdapResponse<Option<LdapEntry>>, VeltrixError> {
if uid.is_empty() {
return Err(VeltrixError::validation("uid", "uid cannot be empty"));
}
let options = SearchOptions::new(
"dc=example,dc=com".into(), format!("(uid={})", self._escape_filter_value(uid)),
);
let result = self.search(options)?;
Ok(LdapResponse::new(
result.data.first().cloned(),
result.backend_used,
))
}
pub fn find_user_by_mail(
&mut self,
mail: &str,
) -> Result<LdapResponse<Vec<LdapEntry>>, VeltrixError> {
if mail.is_empty() {
return Err(VeltrixError::validation("mail", "mail cannot be empty"));
}
let options = SearchOptions::new(
"dc=example,dc=com".into(),
format!("(mail={})", self._escape_filter_value(mail)),
);
self.search(options)
}
pub fn find_users_in_group(
&mut self,
group_dn: &str,
) -> Result<LdapResponse<Vec<LdapEntry>>, VeltrixError> {
if group_dn.is_empty() {
return Err(VeltrixError::validation(
"group_dn",
"group_dn cannot be empty",
));
}
let options = SearchOptions::new(group_dn.to_string(), "(member=*)".to_string());
self.search(options)
}
pub fn find_group_by_cn(
&mut self,
cn: &str,
) -> Result<LdapResponse<Option<LdapEntry>>, VeltrixError> {
if cn.is_empty() {
return Err(VeltrixError::validation("cn", "cn cannot be empty"));
}
let options = SearchOptions::new(
"dc=example,dc=com".into(),
format!("(cn={})", self._escape_filter_value(cn)),
);
let result = self.search(options)?;
Ok(LdapResponse::new(
result.data.first().cloned(),
result.backend_used,
))
}
pub fn find_groups_for_user(
&mut self,
user_dn: &str,
) -> Result<LdapResponse<Vec<LdapEntry>>, VeltrixError> {
if user_dn.is_empty() {
return Err(VeltrixError::validation(
"user_dn",
"user_dn cannot be empty",
));
}
let options = SearchOptions::new(
"dc=example,dc=com".into(),
format!("(member={})", self._escape_filter_value(user_dn)),
);
self.search(options)
}
pub fn get_group_members(
&mut self,
group_dn: &str,
) -> Result<LdapResponse<Vec<String>>, VeltrixError> {
let result = self.get_entry(group_dn)?;
let members = if let Some(entry) = result.data {
entry.get_strings("member").unwrap_or_default()
} else {
vec![]
};
Ok(LdapResponse::new(members, result.backend_used))
}
pub fn find_posix_user_by_uid_number(
&mut self,
uid_number: u32,
) -> Result<LdapResponse<Option<LdapEntry>>, VeltrixError> {
let options = SearchOptions::new(
"dc=example,dc=com".into(),
format!("(uidNumber={})", uid_number),
);
let result = self.search(options)?;
Ok(LdapResponse::new(
result.data.first().cloned(),
result.backend_used,
))
}
pub fn find_posix_group_by_gid_number(
&mut self,
gid_number: u32,
) -> Result<LdapResponse<Option<LdapEntry>>, VeltrixError> {
let options = SearchOptions::new(
"dc=example,dc=com".into(),
format!("(gidNumber={})", gid_number),
);
let result = self.search(options)?;
Ok(LdapResponse::new(
result.data.first().cloned(),
result.backend_used,
))
}
pub fn add_entry(
&mut self,
dn: &str,
attributes: &[(&str, &[&str])],
) -> Result<LdapEmptyResponse, VeltrixError> {
if dn.is_empty() {
return Err(VeltrixError::validation("dn", "DN cannot be empty"));
}
if attributes.is_empty() {
return Err(VeltrixError::validation(
"attributes",
"at least one attribute required",
));
}
self._add_entry_impl(dn, attributes)?;
Ok(LdapEmptyResponse::new(self.backend_used.clone()))
}
pub fn modify_entry(
&mut self,
dn: &str,
changes: &[ModifyOp],
) -> Result<LdapEmptyResponse, VeltrixError> {
if dn.is_empty() {
return Err(VeltrixError::validation("dn", "DN cannot be empty"));
}
if changes.is_empty() {
return Err(VeltrixError::validation(
"changes",
"at least one modification required",
));
}
self._modify_entry_impl(dn, changes)?;
Ok(LdapEmptyResponse::new(self.backend_used.clone()))
}
pub fn delete_entry(&mut self, dn: &str) -> Result<LdapEmptyResponse, VeltrixError> {
if dn.is_empty() {
return Err(VeltrixError::validation("dn", "DN cannot be empty"));
}
self._delete_entry_impl(dn)?;
Ok(LdapEmptyResponse::new(self.backend_used.clone()))
}
pub fn rename_entry(
&mut self,
old_dn: &str,
new_rdn: &str,
new_superior: Option<&str>,
) -> Result<LdapEmptyResponse, VeltrixError> {
if old_dn.is_empty() {
return Err(VeltrixError::validation("old_dn", "DN cannot be empty"));
}
if new_rdn.is_empty() {
return Err(VeltrixError::validation("new_rdn", "RDN cannot be empty"));
}
self._rename_entry_impl(old_dn, new_rdn, new_superior)?;
Ok(LdapEmptyResponse::new(self.backend_used.clone()))
}
pub fn change_password(
&mut self,
user_dn: &str,
old_password: &str,
new_password: &str,
) -> Result<LdapEmptyResponse, VeltrixError> {
if user_dn.is_empty() {
return Err(VeltrixError::validation("user_dn", "DN cannot be empty"));
}
if old_password.is_empty() || new_password.is_empty() {
return Err(VeltrixError::validation(
"password",
"passwords cannot be empty",
));
}
self._change_password_impl(user_dn, old_password, new_password)?;
Ok(LdapEmptyResponse::new(self.backend_used.clone()))
}
pub fn set_password(
&mut self,
user_dn: &str,
new_password: &str,
) -> Result<LdapEmptyResponse, VeltrixError> {
if user_dn.is_empty() {
return Err(VeltrixError::validation("user_dn", "DN cannot be empty"));
}
if new_password.is_empty() {
return Err(VeltrixError::validation(
"password",
"password cannot be empty",
));
}
self._set_password_impl(user_dn, new_password)?;
Ok(LdapEmptyResponse::new(self.backend_used.clone()))
}
#[cfg(feature = "ldap")]
fn _bind_simple(&mut self, bind_dn: &str, password: &str) -> Result<(), VeltrixError> {
let (_host, _port) = self._parse_uri(&self.spec.uri)?;
if bind_dn.is_empty() || password.is_empty() {
return Err(VeltrixError::auth("bind_dn and password cannot be empty"));
}
Ok(())
}
#[cfg(not(feature = "ldap"))]
fn _bind_simple(&mut self, _bind_dn: &str, _password: &str) -> Result<(), VeltrixError> {
Err(VeltrixError::service("ldap", "LDAP feature not enabled"))
}
#[cfg(all(feature = "ldap-sasl", feature = "ldap"))]
fn _bind_sasl_plain(&mut self, identity: &str, password: &str) -> Result<(), VeltrixError> {
if identity.is_empty() || password.is_empty() {
return Err(VeltrixError::auth("identity and password cannot be empty"));
}
Ok(())
}
#[cfg(not(all(feature = "ldap-sasl", feature = "ldap")))]
fn _bind_sasl_plain(&mut self, _identity: &str, _password: &str) -> Result<(), VeltrixError> {
Err(VeltrixError::service(
"ldap",
"SASL/PLAIN feature not enabled",
))
}
#[cfg(all(feature = "ldap-sasl", feature = "ldap"))]
fn _bind_sasl_external(&mut self) -> Result<(), VeltrixError> {
Ok(())
}
#[cfg(not(all(feature = "ldap-sasl", feature = "ldap")))]
fn _bind_sasl_external(&mut self) -> Result<(), VeltrixError> {
Err(VeltrixError::service(
"ldap",
"SASL/EXTERNAL feature not enabled",
))
}
fn _bind_anonymous(&mut self) -> Result<(), VeltrixError> {
Ok(())
}
fn _search_impl(&mut self, options: &SearchOptions) -> Result<Vec<LdapEntry>, VeltrixError> {
if options.base_dn.is_empty() {
return Err(VeltrixError::validation(
"base_dn",
"base DN cannot be empty",
));
}
Ok(vec![])
}
fn _add_entry_impl(
&mut self,
dn: &str,
attributes: &[(&str, &[&str])],
) -> Result<(), VeltrixError> {
if !dn.contains('=') || !dn.contains(',') {
return Err(VeltrixError::validation("dn", "invalid DN format"));
}
let _ = attributes;
Ok(())
}
fn _modify_entry_impl(&mut self, dn: &str, _changes: &[ModifyOp]) -> Result<(), VeltrixError> {
if !dn.contains('=') || !dn.contains(',') {
return Err(VeltrixError::validation("dn", "invalid DN format"));
}
Ok(())
}
fn _delete_entry_impl(&mut self, dn: &str) -> Result<(), VeltrixError> {
if !dn.contains('=') || !dn.contains(',') {
return Err(VeltrixError::validation("dn", "invalid DN format"));
}
Ok(())
}
fn _rename_entry_impl(
&mut self,
_old_dn: &str,
_new_rdn: &str,
_new_superior: Option<&str>,
) -> Result<(), VeltrixError> {
Ok(())
}
fn _change_password_impl(
&mut self,
_user_dn: &str,
_old_password: &str,
_new_password: &str,
) -> Result<(), VeltrixError> {
Ok(())
}
fn _set_password_impl(
&mut self,
_user_dn: &str,
_new_password: &str,
) -> Result<(), VeltrixError> {
Ok(())
}
fn _parse_uri(&self, uri: &str) -> Result<(String, u16), VeltrixError> {
if uri.starts_with("ldap://") {
let rest = uri
.strip_prefix("ldap://")
.ok_or_else(|| VeltrixError::validation("uri", "invalid LDAP URI format"))?;
let parts: Vec<&str> = rest.split(':').collect();
if parts.is_empty() {
return Err(VeltrixError::validation("uri", "missing host in URI"));
}
let host = parts[0].to_string();
let port = if parts.len() > 1 {
parts[1].parse::<u16>().unwrap_or(389)
} else {
389
};
Ok((host, port))
} else if uri.starts_with("ldaps://") {
let rest = uri
.strip_prefix("ldaps://")
.ok_or_else(|| VeltrixError::validation("uri", "invalid LDAPS URI format"))?;
let parts: Vec<&str> = rest.split(':').collect();
if parts.is_empty() {
return Err(VeltrixError::validation("uri", "missing host in URI"));
}
let host = parts[0].to_string();
let port = if parts.len() > 1 {
parts[1].parse::<u16>().unwrap_or(636)
} else {
636
};
Ok((host, port))
} else {
Err(VeltrixError::validation(
"uri",
"URI must start with ldap:// or ldaps://",
))
}
}
fn _escape_filter_value(&self, value: &str) -> String {
value
.replace('\\', "\\5c")
.replace('*', "\\2a")
.replace('(', "\\28")
.replace(')', "\\29")
.replace('\0', "\\00")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_creation() {
let spec = LdapSpec::new(
"ldap://localhost:389".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let client = LdapClient::new(spec);
assert!(client.is_ok());
let client = client.unwrap();
assert!(!client.is_connected());
}
#[test]
fn add_entry_validates_dn() {
let spec = LdapSpec::new(
"ldap://localhost:389".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let mut client = LdapClient::new(spec).unwrap();
let result = client.add_entry("", &[]);
assert!(result.is_err());
}
#[test]
fn add_entry_validates_attributes() {
let spec = LdapSpec::new(
"ldap://localhost:389".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let mut client = LdapClient::new(spec).unwrap();
let result = client.add_entry("cn=test,dc=example,dc=com", &[]);
assert!(result.is_err());
}
#[test]
fn modify_entry_validates_input() {
let spec = LdapSpec::new(
"ldap://localhost:389".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let mut client = LdapClient::new(spec).unwrap();
let result = client.modify_entry("cn=test,dc=example,dc=com", &[]);
assert!(result.is_err());
}
#[test]
fn delete_entry_validates_dn() {
let spec = LdapSpec::new(
"ldap://localhost:389".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let mut client = LdapClient::new(spec).unwrap();
let result = client.delete_entry("");
assert!(result.is_err());
}
#[test]
fn search_validates_filter() {
let spec = LdapSpec::new(
"ldap://localhost:389".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let mut client = LdapClient::new(spec).unwrap();
let options = SearchOptions::new(
"dc=example,dc=com".into(),
"invalid_filter".into(), );
let result = client.search(options);
assert!(result.is_err());
}
#[test]
fn find_user_by_uid_validates_uid() {
let spec = LdapSpec::new(
"ldap://localhost:389".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let mut client = LdapClient::new(spec).unwrap();
let result = client.find_user_by_uid("");
assert!(result.is_err());
}
#[test]
fn uri_parsing_ldap() {
let spec = LdapSpec::new(
"ldap://localhost:389".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let client = LdapClient::new(spec).unwrap();
let (host, port) = client._parse_uri("ldap://example.com:1234").unwrap();
assert_eq!(host, "example.com");
assert_eq!(port, 1234);
}
#[test]
fn uri_parsing_ldaps() {
let spec = LdapSpec::new(
"ldaps://localhost:636".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let client = LdapClient::new(spec).unwrap();
let (host, port) = client._parse_uri("ldaps://example.com").unwrap();
assert_eq!(host, "example.com");
assert_eq!(port, 636);
}
#[test]
fn filter_value_escaping() {
let spec = LdapSpec::new(
"ldap://localhost:389".into(),
super::super::spec::LdapAuthMethod::Anonymous,
);
let client = LdapClient::new(spec).unwrap();
let escaped = client._escape_filter_value("test*value");
assert_eq!(escaped, "test\\2avalue");
}
}