192 lines
5.6 KiB
Rust
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");
|
|
}
|
|
}
|