use dn_escape::dn_escape; use crate::db::User; /// A simple multi-value Map powered by a vector, to respect /// order of first appearance of keys. #[derive(Clone, Debug)] pub struct VecMap { inner: Vec<(String, Vec)>, } impl VecMap { pub fn new() -> Self { Self { inner: vec![] } } pub fn get(&self, key: &str) -> Option<&Vec> { for (prev_key, prev_values) in &self.inner { if key == prev_key { return Some(prev_values); } } None } pub fn insert_or_append(&mut self, key: &str, value: &str) { for (prev_key, prev_values) in &mut self.inner { if key == prev_key { prev_values.push(value.to_string()); return; } } self.inner.push((key.to_string(), vec![value.to_string()])); } pub fn insert(&mut self, key: &str, values: Vec, overwrite: bool) { for (prev_key, prev_values) in &mut self.inner { if key == prev_key { if overwrite { *prev_values = values; } return; } } self.inner.push((key.to_string(), values)); } } #[derive(Clone, Debug)] pub struct MalformedDn; /// A Dn is a key-value mapping which can contain the same key several times. /// /// This implementation is not fully RFC compliant and will only parse simple DNs for /// basic attribute manipulation. /// /// Keys are lowercased, but scrambled keys in a broken order will be reordered. For example, /// `dc=example,ou=people,dc=com` will become `dc=example,dc=com,ou=people`. #[derive(Clone, Debug)] pub struct Dn { pub keys: VecMap, } impl Dn { /// Parse a DN string. Here parsing escaped characters is not critical, if a /// client sends us funny characters, the domain name simply won't match /// and their request won't go anywhere. /// /// However, if we find a really funny request such as `dc=foo=bar`, then /// we return an error to the client. pub fn from_dn_str(input: &str) -> Result { let mut keys = VecMap::new(); if input.is_empty() { return Ok(Self { keys }); } for query in input.split(',') { let mut query_parts = query.split('='); // Here we have key=val pairs if query_parts.clone().count() != 2 { // Bad request return Err(MalformedDn); } let key = query_parts.next().unwrap(); let val = query_parts.next().unwrap(); keys.insert_or_append(key, val); } Ok(Self { keys }) } pub fn to_dn_string(&self) -> String { let mut s = String::new(); let mut first = true; for (key, values) in &self.keys.inner { for value in values { if first { first = false; } else { s.push(','); } s.push_str(key); s.push('='); s.push_str(value); } } s } /// Gets the hostname defined in the `dc` fields of the DN. /// /// For example, `dc=example,dc=com` becomes `Some(example.com)`. /// /// The returned domain is not normalized and may require casing treatment /// to compare meaningfully. pub fn get_hostname(&self) -> Option { let domain_components = self.keys.get("dc")?; // We don't populate the dc key if there was no value at all, so // we have at least one component. let mut domain_components = domain_components.iter(); let mut s = String::from(domain_components.next().unwrap()); for domain_component in domain_components { s.push('.'); s.push_str(domain_component); } Some(s) } /// Overrides the DN hostname (`dc` fields) with the provided host. /// /// When no `dc` fields are present, they are only added when `force` is true. #[expect(unused)] pub fn set_hostname(&mut self, host: &str, force: bool) { let domain_components: Vec = host.split('.').map(|x| dn_escape(x).to_string()).collect(); self.keys.insert("dc", domain_components, force); } pub fn from_user(user: &User) -> Self { let mut keys = VecMap::new(); keys.insert_or_append("uid", &user.username); keys.insert_or_append("ou", "people"); for domain_component in user.domain.split('.') { keys.insert_or_append("dc", domain_component); } Self { keys } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_basic_dn_str() { let s = "cn=admin,ou=people,dc=example,dc=com"; let mut dn = Dn::from_dn_str(s).unwrap(); assert_eq!(&dn.to_dn_string(), s); assert_eq!(dn.get_hostname().as_deref(), Some("example.com")); dn.set_hostname("a.localhost", false); assert_eq!(dn.get_hostname().as_deref(), Some("a.localhost")); assert_eq!(&dn.to_dn_string(), "cn=admin,ou=people,dc=a,dc=localhost"); } #[test] fn test_empty_dn_str() { let s = ""; let mut dn = Dn::from_dn_str(s).unwrap(); assert_eq!(&dn.to_dn_string(), s); assert!(dn.get_hostname().is_none()); dn.set_hostname("a.localhost", false); assert_eq!(dn.get_hostname().as_deref(), None); assert_eq!(&dn.to_dn_string(), s); dn.set_hostname("a.localhost", true); assert_eq!(dn.get_hostname().as_deref(), Some("a.localhost")); assert_eq!(&dn.to_dn_string(), "dc=a,dc=localhost"); } }