llldap/src/ldap/dn.rs

192 lines
5.6 KiB
Rust

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<String>)>,
}
impl VecMap {
pub fn new() -> Self {
Self { inner: vec![] }
}
pub fn get(&self, key: &str) -> Option<&Vec<String>> {
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<String>, 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<Self, MalformedDn> {
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<String> {
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<String> =
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");
}
}