use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SearchScope {
Base,
OneLevel,
Subtree,
}
#[derive(Debug, Clone)]
pub struct LdapEntry {
pub dn: String,
pub attributes: HashMap<String, LdapAttribute>,
}
impl LdapEntry {
pub fn new(dn: String) -> Self {
Self {
dn,
attributes: HashMap::new(),
}
}
pub fn get_string(&self, attr: &str) -> Option<String> {
let key = attr.to_lowercase();
self.attributes.get(&key).and_then(|attr| {
attr.values
.first()
.and_then(|v| String::from_utf8(v.clone()).ok())
})
}
pub fn get_strings(&self, attr: &str) -> Option<Vec<String>> {
let key = attr.to_lowercase();
self.attributes.get(&key).map(|attr| {
attr.values
.iter()
.filter_map(|v| String::from_utf8(v.clone()).ok())
.collect()
})
}
pub fn get_bytes(&self, attr: &str) -> Option<Vec<u8>> {
let key = attr.to_lowercase();
self.attributes
.get(&key)
.and_then(|attr| attr.values.first().cloned())
}
pub fn get_all_bytes(&self, attr: &str) -> Option<Vec<Vec<u8>>> {
let key = attr.to_lowercase();
self.attributes.get(&key).map(|attr| attr.values.clone())
}
}
#[derive(Debug, Clone)]
pub struct LdapAttribute {
pub name: String,
pub values: Vec<Vec<u8>>,
}
impl LdapAttribute {
pub fn new(name: String, values: Vec<Vec<u8>>) -> Self {
Self {
name: name.to_lowercase(),
values,
}
}
pub fn from_strings(name: String, values: Vec<String>) -> Self {
Self {
name: name.to_lowercase(),
values: values.into_iter().map(|s| s.into_bytes()).collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct SearchOptions {
pub base_dn: String,
pub filter: String,
pub scope: SearchScope,
pub attributes: Option<Vec<String>>,
pub size_limit: Option<usize>,
pub time_limit: Option<Duration>,
pub paged_size: Option<usize>,
pub types_only: bool,
}
impl SearchOptions {
pub fn new(base_dn: String, filter: String) -> Self {
Self {
base_dn,
filter,
scope: SearchScope::Subtree,
attributes: None,
size_limit: None,
time_limit: None,
paged_size: Some(500),
types_only: false,
}
}
pub fn with_scope(mut self, scope: SearchScope) -> Self {
self.scope = scope;
self
}
pub fn with_attributes(mut self, attributes: Vec<String>) -> Self {
self.attributes = Some(attributes);
self
}
pub fn with_size_limit(mut self, limit: usize) -> Self {
self.size_limit = Some(limit);
self
}
pub fn with_time_limit(mut self, timeout: Duration) -> Self {
self.time_limit = Some(timeout);
self
}
pub fn with_page_size(mut self, size: usize) -> Self {
self.paged_size = Some(size);
self
}
pub fn with_types_only(mut self, types_only: bool) -> Self {
self.types_only = types_only;
self
}
}
#[derive(Debug, Clone)]
pub enum ModifyOp {
Add {
attr: String,
values: Vec<Vec<u8>>,
},
Replace {
attr: String,
values: Vec<Vec<u8>>,
},
Delete {
attr: String,
values: Option<Vec<Vec<u8>>>,
},
}
impl ModifyOp {
pub fn add_strings(attr: String, values: Vec<String>) -> Self {
Self::Add {
attr,
values: values.into_iter().map(|s| s.into_bytes()).collect(),
}
}
pub fn replace_strings(attr: String, values: Vec<String>) -> Self {
Self::Replace {
attr,
values: values.into_iter().map(|s| s.into_bytes()).collect(),
}
}
pub fn delete_attr(attr: String) -> Self {
Self::Delete { attr, values: None }
}
pub fn delete_values(attr: String, values: Vec<Vec<u8>>) -> Self {
Self::Delete {
attr,
values: Some(values),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_get_string() {
let mut entry = LdapEntry::new("cn=test,dc=example,dc=com".into());
entry.attributes.insert(
"cn".into(),
LdapAttribute::from_strings("cn".into(), vec!["test".into()]),
);
assert_eq!(entry.get_string("cn"), Some("test".into()));
assert_eq!(entry.get_string("mail"), None);
}
#[test]
fn search_scope_operations() {
let mut opts = SearchOptions::new("dc=example,dc=com".into(), "(uid=*)".into());
assert_eq!(opts.scope, SearchScope::Subtree);
opts = opts.with_scope(SearchScope::Base);
assert_eq!(opts.scope, SearchScope::Base);
}
#[test]
fn modify_operations() {
let add = ModifyOp::add_strings("mail".into(), vec!["test@example.com".into()]);
if let ModifyOp::Add { attr, values } = add {
assert_eq!(attr, "mail");
assert_eq!(values.len(), 1);
} else {
panic!("Expected Add operation");
}
let delete = ModifyOp::delete_attr("description".into());
if let ModifyOp::Delete { attr, values } = delete {
assert_eq!(attr, "description");
assert_eq!(values, None);
} else {
panic!("Expected Delete operation");
}
}
}