Add ability to list users with an LDAP search request

This commit is contained in:
Valentin Tolmer 2021-03-16 18:27:31 +01:00
parent 5a1dfa3d65
commit cda2bcacc3
5 changed files with 278 additions and 39 deletions

View File

@ -12,6 +12,7 @@ actix-service = "2.0.0-beta.4"
actix-web = "4.0.0-beta.3" actix-web = "4.0.0-beta.3"
anyhow = "*" anyhow = "*"
clap = "3.0.0-beta.2" clap = "3.0.0-beta.2"
chrono = "*"
futures = "*" futures = "*"
futures-util = "*" futures-util = "*"
http = "*" http = "*"

View File

@ -9,14 +9,25 @@ pub struct BindRequest {
} }
#[cfg_attr(test, derive(PartialEq, Eq, Debug))] #[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct SearchRequest {} pub struct ListUsersRequest {
// filters
pub attrs: Vec<String>,
}
#[cfg_attr(test, derive(PartialEq, Eq, Debug))] #[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct SearchResponse {} pub struct User {
pub user_id: String,
pub email: String,
pub display_name: String,
pub first_name: String,
pub last_name: String,
// pub avatar: ?,
pub creation_date: chrono::NaiveDateTime,
}
pub trait BackendHandler: Clone + Send { pub trait BackendHandler: Clone + Send {
fn bind(&mut self, request: BindRequest) -> Result<()>; fn bind(&mut self, request: BindRequest) -> Result<()>;
fn search(&mut self, request: SearchRequest) -> Result<SearchResponse>; fn list_users(&mut self, request: ListUsersRequest) -> Result<Vec<User>>;
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -48,8 +59,8 @@ impl BackendHandler for SqlBackendHandler {
} }
} }
fn search(&mut self, request: SearchRequest) -> Result<SearchResponse> { fn list_users(&mut self, request: ListUsersRequest) -> Result<Vec<User>> {
Ok(SearchResponse {}) Ok(Vec::new())
} }
} }
@ -61,6 +72,6 @@ mockall::mock! {
} }
impl BackendHandler for TestBackendHandler { impl BackendHandler for TestBackendHandler {
fn bind(&mut self, request: BindRequest) -> Result<()>; fn bind(&mut self, request: BindRequest) -> Result<()>;
fn search(&mut self, request: SearchRequest) -> Result<SearchResponse>; fn list_users(&mut self, request: ListUsersRequest) -> Result<Vec<User>>;
} }
} }

View File

@ -13,6 +13,7 @@ pub struct Configuration {
pub ldaps_port: u16, pub ldaps_port: u16,
pub http_port: u16, pub http_port: u16,
pub secret_pepper: String, pub secret_pepper: String,
pub ldap_base_dn: String,
pub ldap_user_dn: String, pub ldap_user_dn: String,
pub ldap_user_pass: String, pub ldap_user_pass: String,
pub database_url: String, pub database_url: String,
@ -26,6 +27,7 @@ impl Default for Configuration {
ldaps_port: 6360, ldaps_port: 6360,
http_port: 17170, http_port: 17170,
secret_pepper: String::from("secretsecretpepper"), secret_pepper: String::from("secretsecretpepper"),
ldap_base_dn: String::from("dc=example,dc=com"),
ldap_user_dn: String::new(), ldap_user_dn: String::new(),
ldap_user_pass: String::new(), ldap_user_pass: String::new(),
database_url: String::from("sqlite://users.db?mode=rwc"), database_url: String::from("sqlite://users.db?mode=rwc"),

View File

@ -1,16 +1,102 @@
use crate::domain::handler::BackendHandler; use crate::domain::handler::{BackendHandler, ListUsersRequest, User};
use anyhow::{bail, Result};
use ldap3_server::simple::*; use ldap3_server::simple::*;
fn make_dn_pair<'a, I>(mut iter: I) -> Result<(String, String)>
where
I: Iterator<Item = String>,
{
let pair = (
iter.next()
.ok_or(anyhow::Error::msg("Empty DN element"))?
.clone(),
iter.next()
.ok_or(anyhow::Error::msg("Missing DN value"))?
.clone(),
);
if let Some(e) = iter.next() {
bail!(
r#"Too many elements in distinguished name: "{:?}", "{:?}", "{:?}""#,
pair.0,
pair.1,
e
)
}
Ok(pair)
}
fn parse_distinguished_name(dn: &str) -> Result<Vec<(String, String)>> {
dn.split(",")
.map(|s| make_dn_pair(s.split("=").map(String::from)))
.collect()
}
fn make_ldap_search_result_entry(user: User, base_dn_str: &str) -> LdapSearchResultEntry {
LdapSearchResultEntry {
dn: format!("cn={},{}", user.user_id, base_dn_str),
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec![
"inetOrgPerson".to_string(),
"posixAccount".to_string(),
"mailAccount".to_string(),
],
},
LdapPartialAttribute {
atype: "uid".to_string(),
vals: vec![user.user_id],
},
LdapPartialAttribute {
atype: "mail".to_string(),
vals: vec![user.email],
},
LdapPartialAttribute {
atype: "givenName".to_string(),
vals: vec![user.first_name],
},
LdapPartialAttribute {
atype: "sn".to_string(),
vals: vec![user.last_name],
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec![user.display_name],
},
],
}
}
fn is_subtree(subtree: &Vec<(String, String)>, base_tree: &Vec<(String, String)>) -> bool {
if subtree.len() < base_tree.len() {
return false;
}
let size_diff = subtree.len() - base_tree.len();
for i in 0..base_tree.len() {
if subtree[size_diff + i] != base_tree[i] {
return false;
}
}
true
}
pub struct LdapHandler<Backend: BackendHandler> { pub struct LdapHandler<Backend: BackendHandler> {
dn: String, dn: String,
backend_handler: Backend, backend_handler: Backend,
pub base_dn: Vec<(String, String)>,
base_dn_str: String,
} }
impl<Backend: BackendHandler> LdapHandler<Backend> { impl<Backend: BackendHandler> LdapHandler<Backend> {
pub fn new(backend_handler: Backend) -> Self { pub fn new(backend_handler: Backend, ldap_base_dn: String) -> Self {
Self { Self {
dn: "Unauthenticated".to_string(), dn: "Unauthenticated".to_string(),
backend_handler, backend_handler,
base_dn: parse_distinguished_name(&ldap_base_dn).expect(&format!(
"Invalid value for ldap_base_dn in configuration: {}",
ldap_base_dn
)),
base_dn_str: ldap_base_dn,
} }
} }
@ -30,35 +116,35 @@ impl<Backend: BackendHandler> LdapHandler<Backend> {
} }
pub fn do_search(&mut self, lsr: &SearchRequest) -> Vec<LdapMsg> { pub fn do_search(&mut self, lsr: &SearchRequest) -> Vec<LdapMsg> {
vec![ let dn_parts = match parse_distinguished_name(&lsr.base) {
lsr.gen_result_entry(LdapSearchResultEntry { Ok(dn) => dn,
dn: "cn=hello,dc=example,dc=com".to_string(), Err(_) => {
attributes: vec![ return vec![lsr.gen_error(
LdapPartialAttribute { LdapResultCode::OperationsError,
atype: "objectClass".to_string(), format!(r#"Could not parse base DN: "{}""#, lsr.base),
vals: vec!["cursed".to_string()], )]
}, }
LdapPartialAttribute { };
atype: "cn".to_string(), if !is_subtree(&dn_parts, &self.base_dn) {
vals: vec!["hello".to_string()], return vec![lsr.gen_success()];
}, }
], let users = match self.backend_handler.list_users(ListUsersRequest {
}), attrs: lsr.attrs.clone(),
lsr.gen_result_entry(LdapSearchResultEntry { }) {
dn: "cn=world,dc=example,dc=com".to_string(), Ok(users) => users,
attributes: vec![ Err(e) => {
LdapPartialAttribute { return vec![lsr.gen_error(
atype: "objectClass".to_string(), LdapResultCode::Other,
vals: vec!["cursed".to_string()], format!(r#"Error during search for "{}": {}"#, lsr.base, e),
}, )]
LdapPartialAttribute { }
atype: "cn".to_string(), };
vals: vec!["world".to_string()], let mut res = users
}, .into_iter()
], .map(|u| lsr.gen_result_entry(make_ldap_search_result_entry(u, &self.base_dn_str)))
}), .collect::<Vec<_>>();
lsr.gen_success(), res.push(lsr.gen_success());
] res
} }
pub fn do_whoami(&mut self, wr: &WhoamiRequest) -> LdapMsg { pub fn do_whoami(&mut self, wr: &WhoamiRequest) -> LdapMsg {
@ -87,6 +173,7 @@ impl<Backend: BackendHandler> LdapHandler<Backend> {
mod tests { mod tests {
use super::*; use super::*;
use crate::domain::handler::MockTestBackendHandler; use crate::domain::handler::MockTestBackendHandler;
use chrono::NaiveDateTime;
use mockall::predicate::eq; use mockall::predicate::eq;
#[test] #[test]
@ -99,7 +186,7 @@ mod tests {
})) }))
.times(1) .times(1)
.return_once(|_| Ok(())); .return_once(|_| Ok(()));
let mut ldap_handler = LdapHandler::new(mock); let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string());
let request = WhoamiRequest { msgid: 1 }; let request = WhoamiRequest { msgid: 1 };
assert_eq!( assert_eq!(
@ -120,4 +207,139 @@ mod tests {
request.gen_success("dn: test") request.gen_success("dn: test")
); );
} }
#[test]
fn test_is_subtree() {
let subtree1 = vec![
("ou".to_string(), "people".to_string()),
("dc".to_string(), "example".to_string()),
("dc".to_string(), "com".to_string()),
];
let root = vec![
("dc".to_string(), "example".to_string()),
("dc".to_string(), "com".to_string()),
];
assert!(is_subtree(&subtree1, &root));
assert!(!is_subtree(&vec![], &root));
}
#[test]
fn test_parse_distinguished_name() {
let parsed_dn = vec![
("ou".to_string(), "people".to_string()),
("dc".to_string(), "example".to_string()),
("dc".to_string(), "com".to_string()),
];
assert_eq!(
parse_distinguished_name("ou=people,dc=example,dc=com").expect("parsing failed"),
parsed_dn
);
}
#[test]
fn test_search() {
let mut mock = MockTestBackendHandler::new();
mock.expect_bind().return_once(|_| Ok(()));
mock.expect_list_users()
.with(eq(ListUsersRequest { attrs: vec![] }))
.times(1)
.return_once(|_| {
Ok(vec![
User {
user_id: "bob_1".to_string(),
email: "bob@bobmail.bob".to_string(),
display_name: "Bôb Böbberson".to_string(),
first_name: "Bôb".to_string(),
last_name: "Böbberson".to_string(),
creation_date: NaiveDateTime::from_timestamp(1_000_000_000, 0),
},
User {
user_id: "jim".to_string(),
email: "jim@cricket.jim".to_string(),
display_name: "Jimminy Cricket".to_string(),
first_name: "Jim".to_string(),
last_name: "Cricket".to_string(),
creation_date: NaiveDateTime::from_timestamp(1_003_000_000, 0),
},
])
});
let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string());
let request = SimpleBindRequest {
msgid: 1,
dn: "test".to_string(),
pw: "pass".to_string(),
};
assert_eq!(ldap_handler.do_bind(&request), request.gen_success());
let request = SearchRequest {
msgid: 2,
base: "ou=people,dc=example,dc=com".to_string(),
scope: LdapSearchScope::Base,
filter: LdapFilter::And(vec![]),
attrs: vec![],
};
assert_eq!(
ldap_handler.do_search(&request),
vec![
request.gen_result_entry(LdapSearchResultEntry {
dn: "cn=bob_1,dc=example,dc=com".to_string(),
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["inetOrgPerson".to_string(), "posixAccount".to_string(), "mailAccount".to_string()]
},
LdapPartialAttribute {
atype: "uid".to_string(),
vals: vec!["bob_1".to_string()]
},
LdapPartialAttribute {
atype: "mail".to_string(),
vals: vec!["bob@bobmail.bob".to_string()]
},
LdapPartialAttribute {
atype: "givenName".to_string(),
vals: vec!["Bôb".to_string()]
},
LdapPartialAttribute {
atype: "sn".to_string(),
vals: vec!["Böbberson".to_string()]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["Bôb Böbberson".to_string()]
}
],
}),
request.gen_result_entry(LdapSearchResultEntry {
dn: "cn=jim,dc=example,dc=com".to_string(),
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["inetOrgPerson".to_string(), "posixAccount".to_string(), "mailAccount".to_string()]
},
LdapPartialAttribute {
atype: "uid".to_string(),
vals: vec!["jim".to_string()]
},
LdapPartialAttribute {
atype: "mail".to_string(),
vals: vec!["jim@cricket.jim".to_string()]
},
LdapPartialAttribute {
atype: "givenName".to_string(),
vals: vec!["Jim".to_string()]
},
LdapPartialAttribute {
atype: "sn".to_string(),
vals: vec!["Cricket".to_string()]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["Jimminy Cricket".to_string()]
}
],
}),
request.gen_success()
]
);
}
} }

View File

@ -64,18 +64,21 @@ where
{ {
use futures_util::StreamExt; use futures_util::StreamExt;
let ldap_base_dn = config.ldap_base_dn.clone();
Ok( Ok(
server_builder.bind("ldap", ("0.0.0.0", config.ldap_port), move || { server_builder.bind("ldap", ("0.0.0.0", config.ldap_port), move || {
let backend_handler = backend_handler.clone(); let backend_handler = backend_handler.clone();
let ldap_base_dn = ldap_base_dn.clone();
pipeline_factory(fn_service(move |mut stream: TcpStream| { pipeline_factory(fn_service(move |mut stream: TcpStream| {
let backend_handler = backend_handler.clone(); let backend_handler = backend_handler.clone();
let ldap_base_dn = ldap_base_dn.clone();
async move { async move {
// Configure the codec etc. // Configure the codec etc.
let (r, w) = stream.split(); let (r, w) = stream.split();
let mut requests = FramedRead::new(r, LdapCodec); let mut requests = FramedRead::new(r, LdapCodec);
let mut resp = FramedWrite::new(w, LdapCodec); let mut resp = FramedWrite::new(w, LdapCodec);
let mut session = LdapHandler::new(backend_handler); let mut session = LdapHandler::new(backend_handler, ldap_base_dn);
while let Some(msg) = requests.next().await { while let Some(msg) = requests.next().await {
if !handle_incoming_message(msg, &mut resp, &mut session).await? { if !handle_incoming_message(msg, &mut resp, &mut session).await? {