feat: Implement LDAP bind/whoami
This commit is contained in:
parent
bdb5008914
commit
b4330b37fa
19 changed files with 664 additions and 37 deletions
222
src/ldap/dn.rs
Normal file
222
src/ldap/dn.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
use dn_escape::dn_escape;
|
||||
use ldap3_proto::LdapResultCode;
|
||||
|
||||
use crate::db::UserRef;
|
||||
use crate::ldap::LdapReturnError;
|
||||
|
||||
/// 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 InvalidDnError(pub String);
|
||||
|
||||
impl LdapReturnError for InvalidDnError {
|
||||
fn code(&self) -> LdapResultCode {
|
||||
LdapResultCode::InvalidDNSyntax
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
format!("Invalid dn: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NotUserDnError(pub String);
|
||||
|
||||
impl LdapReturnError for NotUserDnError {
|
||||
fn code(&self) -> LdapResultCode {
|
||||
LdapResultCode::InvalidDNSyntax
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
format!("Not a user dn containing uid/dc: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, InvalidDnError> {
|
||||
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(InvalidDnError(input.to_string()));
|
||||
}
|
||||
|
||||
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.
|
||||
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 to_user_ref(&self) -> Result<UserRef, NotUserDnError> {
|
||||
let Some(username_vals) = self.keys.get("uid") else {
|
||||
return Err(NotUserDnError(self.to_dn_string()));
|
||||
};
|
||||
if username_vals.len() != 1 {
|
||||
return Err(NotUserDnError(self.to_dn_string()));
|
||||
}
|
||||
let username = username_vals[0].clone();
|
||||
|
||||
let Some(domain_vals) = self.keys.get("dc") else {
|
||||
return Err(NotUserDnError(self.to_dn_string()));
|
||||
};
|
||||
let domain = domain_vals.join(".");
|
||||
|
||||
Ok(UserRef { username, domain })
|
||||
}
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue