mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
Add ability to list users with an LDAP search request
This commit is contained in:
parent
5a1dfa3d65
commit
cda2bcacc3
@ -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 = "*"
|
||||||
|
@ -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>>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"),
|
||||||
|
@ -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()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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? {
|
||||||
|
Loading…
Reference in New Issue
Block a user