diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index ebb1233..26a67f9 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -1,10 +1,13 @@ use crate::domain::handler::{ - BackendHandler, Group, GroupIdAndName, LoginHandler, RequestFilter, User, + BackendHandler, BindRequest, Group, GroupIdAndName, LoginHandler, RequestFilter, User, }; use anyhow::{bail, Result}; use futures::stream::StreamExt; use futures_util::TryStreamExt; -use ldap3_server::simple::*; +use ldap3_server::proto::{ + LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedResponse, LdapFilter, LdapOp, + LdapPartialAttribute, LdapResult, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, +}; use log::*; fn make_dn_pair(mut iter: I) -> Result<(String, String)> @@ -194,6 +197,19 @@ fn map_field(field: &str) -> Result { }) } +fn make_search_success() -> LdapOp { + make_search_error(LdapResultCode::Success, "".to_string()) +} + +fn make_search_error(code: LdapResultCode, message: String) -> LdapOp { + LdapOp::SearchResultDone(LdapResult { + code, + matcheddn: "".to_string(), + message, + referral: vec![], + }) +} + pub struct LdapHandler { dn: String, backend_handler: Backend, @@ -218,33 +234,40 @@ impl LdapHandler { } } - pub async fn do_bind(&mut self, sbr: &SimpleBindRequest) -> LdapMsg { - info!(r#"Received bind request for "{}""#, &sbr.dn); - let user_id = - match get_user_id_from_distinguished_name(&sbr.dn, &self.base_dn, &self.base_dn_str) { - Ok(s) => s, - Err(e) => return sbr.gen_error(LdapResultCode::NamingViolation, e.to_string()), - }; + pub async fn do_bind(&mut self, request: &LdapBindRequest) -> (LdapResultCode, String) { + info!(r#"Received bind request for "{}""#, &request.dn); + let user_id = match get_user_id_from_distinguished_name( + &request.dn, + &self.base_dn, + &self.base_dn_str, + ) { + Ok(s) => s, + Err(e) => return (LdapResultCode::NamingViolation, e.to_string()), + }; + let LdapBindCred::Simple(password) = &request.cred; match self .backend_handler - .bind(crate::domain::handler::BindRequest { + .bind(BindRequest { name: user_id, - password: sbr.pw.clone(), + password: password.clone(), }) .await { Ok(()) => { - self.dn = sbr.dn.clone(); - sbr.gen_success() + self.dn = request.dn.clone(); + (LdapResultCode::Success, "".to_string()) } - Err(_) => sbr.gen_invalid_cred(), + Err(_) => (LdapResultCode::InvalidCredentials, "".to_string()), } } - pub async fn do_search(&mut self, lsr: &SearchRequest) -> Vec { - info!("Received search request with filters: {:?}", &lsr.filter); + pub async fn do_search(&mut self, request: &LdapSearchRequest) -> Vec { + info!( + "Received search request with filters: {:?}", + &request.filter + ); if self.dn != self.ldap_user_dn { - return vec![lsr.gen_error( + return vec![make_search_error( LdapResultCode::InsufficentAccessRights, format!( r#"Current user `{}` is not allowed to query LDAP, expected {}"#, @@ -252,43 +275,43 @@ impl LdapHandler { ), )]; } - let dn_parts = if lsr.base.is_empty() { + let dn_parts = if request.base.is_empty() { self.base_dn.clone() } else { - match parse_distinguished_name(&lsr.base) { + match parse_distinguished_name(&request.base) { Ok(dn) => dn, Err(_) => { - return vec![lsr.gen_error( + return vec![make_search_error( LdapResultCode::OperationsError, - format!(r#"Could not parse base DN: "{}""#, lsr.base), + format!(r#"Could not parse base DN: "{}""#, request.base), )] } } }; if !is_subtree(&dn_parts, &self.base_dn) { // Search path is not in our tree, just return an empty success. - return vec![lsr.gen_success()]; + return vec![make_search_success()]; } let mut results = Vec::new(); if dn_parts.len() == self.base_dn.len() || (dn_parts.len() == self.base_dn.len() + 1 && dn_parts[0] == ("ou".to_string(), "people".to_string())) { - results.extend(self.get_user_list(lsr).await); + results.extend(self.get_user_list(request).await); } if dn_parts.len() == self.base_dn.len() + 1 && dn_parts[0] == ("ou".to_string(), "groups".to_string()) { - results.extend(self.get_groups_list(lsr).await); + results.extend(self.get_groups_list(request).await); } results } - async fn get_user_list(&self, lsr: &SearchRequest) -> Vec { - let filters = match self.convert_user_filter(&lsr.filter) { + async fn get_user_list(&self, request: &LdapSearchRequest) -> Vec { + let filters = match self.convert_user_filter(&request.filter) { Ok(f) => Some(f), Err(e) => { - return vec![lsr.gen_error( + return vec![make_search_error( LdapResultCode::UnwillingToPerform, format!("Unsupported user filter: {}", e), )] @@ -297,28 +320,33 @@ impl LdapHandler { let users = match self.backend_handler.list_users(filters).await { Ok(users) => users, Err(e) => { - return vec![lsr.gen_error( + return vec![make_search_error( LdapResultCode::Other, - format!(r#"Error during searching user "{}": {}"#, lsr.base, e), + format!(r#"Error during searching user "{}": {}"#, request.base, e), )] } }; users .into_iter() - .map(|u| make_ldap_search_user_result_entry(u, &self.base_dn_str, &lsr.attrs)) - .map(|entry| Ok(lsr.gen_result_entry(entry?))) + .map(|u| make_ldap_search_user_result_entry(u, &self.base_dn_str, &request.attrs)) + .map(|entry| Ok(LdapOp::SearchResultEntry(entry?))) // If the processing succeeds, add a success message at the end. - .chain(std::iter::once(Ok(lsr.gen_success()))) + .chain(std::iter::once(Ok(make_search_success()))) .collect::>>() - .unwrap_or_else(|e| vec![lsr.gen_error(LdapResultCode::NoSuchAttribute, e.to_string())]) + .unwrap_or_else(|e| { + vec![make_search_error( + LdapResultCode::NoSuchAttribute, + e.to_string(), + )] + }) } - async fn get_groups_list(&self, lsr: &SearchRequest) -> Vec { - let for_user = match self.get_group_filter(&lsr.filter) { + async fn get_groups_list(&self, request: &LdapSearchRequest) -> Vec { + let for_user = match self.get_group_filter(&request.filter) { Ok(u) => u, Err(e) => { - return vec![lsr.gen_error( + return vec![make_search_error( LdapResultCode::UnwillingToPerform, format!("Unsupported group filter: {}", e), )] @@ -343,9 +371,12 @@ impl LdapHandler { let groups_without_users = match self.backend_handler.get_user_groups(&user).await { Ok(groups) => groups, Err(e) => { - return vec![lsr.gen_error( + return vec![make_search_error( LdapResultCode::Other, - format!(r#"Error while listing user groups: "{}": {}"#, lsr.base, e), + format!( + r#"Error while listing user groups: "{}": {}"#, + request.base, e + ), )] } }; @@ -356,9 +387,9 @@ impl LdapHandler { { Ok(groups) => groups, Err(e) => { - return vec![lsr.gen_error( + return vec![make_search_error( LdapResultCode::Other, - format!(r#"Error while listing user groups: "{}": {}"#, lsr.base, e), + format!(r#"Error while listing user groups: "{}": {}"#, request.base, e), )] } } @@ -366,9 +397,9 @@ impl LdapHandler { match self.backend_handler.list_groups().await { Ok(groups) => groups, Err(e) => { - return vec![lsr.gen_error( + return vec![make_search_error( LdapResultCode::Other, - format!(r#"Error while listing groups "{}": {}"#, lsr.base, e), + format!(r#"Error while listing groups "{}": {}"#, request.base, e), )] } } @@ -376,31 +407,49 @@ impl LdapHandler { groups .into_iter() - .map(|u| make_ldap_search_group_result_entry(u, &self.base_dn_str, &lsr.attrs)) - .map(|entry| Ok(lsr.gen_result_entry(entry?))) + .map(|u| make_ldap_search_group_result_entry(u, &self.base_dn_str, &request.attrs)) + .map(|entry| Ok(LdapOp::SearchResultEntry(entry?))) // If the processing succeeds, add a success message at the end. - .chain(std::iter::once(Ok(lsr.gen_success()))) + .chain(std::iter::once(Ok(make_search_success()))) .collect::>>() - .unwrap_or_else(|e| vec![lsr.gen_error(LdapResultCode::NoSuchAttribute, e.to_string())]) + .unwrap_or_else(|e| { + vec![make_search_error( + LdapResultCode::NoSuchAttribute, + e.to_string(), + )] + }) } - pub fn do_whoami(&mut self, wr: &WhoamiRequest) -> LdapMsg { - if self.dn == "Unauthenticated" { - wr.gen_operror("Unauthenticated") - } else { - wr.gen_success(format!("dn: {}", self.dn).as_str()) - } - } - - pub async fn handle_ldap_message(&mut self, server_op: ServerOps) -> Option> { - Some(match server_op { - ServerOps::SimpleBind(sbr) => vec![self.do_bind(&sbr).await], - ServerOps::Search(sr) => self.do_search(&sr).await, - ServerOps::Unbind(_) => { + pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option> { + Some(match ldap_op { + LdapOp::BindRequest(request) => { + let (code, message) = self.do_bind(&request).await; + vec![LdapOp::BindResponse(LdapBindResponse { + res: LdapResult { + code, + matcheddn: "".to_string(), + message, + referral: vec![], + }, + saslcreds: None, + })] + } + LdapOp::SearchRequest(request) => self.do_search(&request).await, + LdapOp::UnbindRequest => { + self.dn = "Unauthenticated".to_string(); // No need to notify on unbind (per rfc4511) return None; } - ServerOps::Whoami(wr) => vec![self.do_whoami(&wr)], + op => vec![LdapOp::ExtendedResponse(LdapExtendedResponse { + res: LdapResult { + code: LdapResultCode::UnwillingToPerform, + matcheddn: "".to_string(), + message: format!("Unsupported operation: {:#?}", op), + referral: vec![], + }, + name: None, + value: None, + })], }) } @@ -480,9 +529,26 @@ impl LdapHandler { mod tests { use super::*; use crate::domain::handler::{BindRequest, MockTestBackendHandler}; + use ldap3_server::proto::{LdapDerefAliases, LdapSearchScope}; use mockall::predicate::eq; use tokio; + fn make_search_request>( + filter: LdapFilter, + attrs: Vec, + ) -> LdapSearchRequest { + LdapSearchRequest { + base: "ou=people,dc=example,dc=com".to_string(), + scope: LdapSearchScope::Base, + aliases: LdapDerefAliases::Never, + sizelimit: 0, + timelimit: 0, + typesonly: false, + filter, + attrs: attrs.into_iter().map(Into::into).collect(), + } + } + async fn setup_bound_handler( mut mock: MockTestBackendHandler, ) -> LdapHandler { @@ -494,12 +560,14 @@ mod tests { .return_once(|_| Ok(())); let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string(), "test".to_string()); - let request = SimpleBindRequest { - msgid: 1, + let request = LdapBindRequest { dn: "cn=test,ou=people,dc=example,dc=com".to_string(), - pw: "pass".to_string(), + cred: LdapBindCred::Simple("pass".to_string()), }; - assert_eq!(ldap_handler.do_bind(&request).await, request.gen_success()); + assert_eq!( + ldap_handler.do_bind(&request).await.0, + LdapResultCode::Success + ); ldap_handler } @@ -516,23 +584,13 @@ mod tests { let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string(), "test".to_string()); - let request = WhoamiRequest { msgid: 1 }; - assert_eq!( - ldap_handler.do_whoami(&request), - request.gen_operror("Unauthenticated") - ); - - let request = SimpleBindRequest { - msgid: 2, + let request = LdapBindRequest { dn: "cn=bob,ou=people,dc=example,dc=com".to_string(), - pw: "pass".to_string(), + cred: LdapBindCred::Simple("pass".to_string()), }; - assert_eq!(ldap_handler.do_bind(&request).await, request.gen_success()); - - let request = WhoamiRequest { msgid: 3 }; assert_eq!( - ldap_handler.do_whoami(&request), - request.gen_success("dn: cn=bob,ou=people,dc=example,dc=com") + ldap_handler.do_bind(&request).await.0, + LdapResultCode::Success ); } @@ -549,23 +607,13 @@ mod tests { let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string(), "test".to_string()); - let request = WhoamiRequest { msgid: 1 }; - assert_eq!( - ldap_handler.do_whoami(&request), - request.gen_operror("Unauthenticated") - ); - - let request = SimpleBindRequest { - msgid: 2, + let request = LdapBindRequest { dn: "cn=test,ou=people,dc=example,dc=com".to_string(), - pw: "pass".to_string(), + cred: LdapBindCred::Simple("pass".to_string()), }; - assert_eq!(ldap_handler.do_bind(&request).await, request.gen_success()); - - let request = WhoamiRequest { msgid: 3 }; assert_eq!( - ldap_handler.do_whoami(&request), - request.gen_success("dn: cn=test,ou=people,dc=example,dc=com") + ldap_handler.do_bind(&request).await.0, + LdapResultCode::Success ); } @@ -582,35 +630,19 @@ mod tests { let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string(), "admin".to_string()); - let request = WhoamiRequest { msgid: 1 }; - assert_eq!( - ldap_handler.do_whoami(&request), - request.gen_operror("Unauthenticated") - ); - - let request = SimpleBindRequest { - msgid: 2, + let request = LdapBindRequest { dn: "cn=test,ou=people,dc=example,dc=com".to_string(), - pw: "pass".to_string(), + cred: LdapBindCred::Simple("pass".to_string()), }; - assert_eq!(ldap_handler.do_bind(&request).await, request.gen_success()); - - let request = WhoamiRequest { msgid: 3 }; assert_eq!( - ldap_handler.do_whoami(&request), - request.gen_success("dn: cn=test,ou=people,dc=example,dc=com") + ldap_handler.do_bind(&request).await.0, + LdapResultCode::Success ); - let request = SearchRequest { - msgid: 2, - base: "ou=people,dc=example,dc=com".to_string(), - scope: LdapSearchScope::Base, - filter: LdapFilter::And(vec![]), - attrs: vec![], - }; + let request = make_search_request::(LdapFilter::And(vec![]), vec![]); assert_eq!( ldap_handler.do_search(&request).await, - vec![request.gen_error( + vec![make_search_error( LdapResultCode::InsufficentAccessRights, r#"Current user `cn=test,ou=people,dc=example,dc=com` is not allowed to query LDAP, expected cn=admin,ou=people,dc=example,dc=com"#.to_string() )] @@ -623,30 +655,21 @@ mod tests { let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string(), "admin".to_string()); - let request = SimpleBindRequest { - msgid: 2, + let request = LdapBindRequest { dn: "cn=bob,dc=example,dc=com".to_string(), - pw: "pass".to_string(), + cred: LdapBindCred::Simple("pass".to_string()), }; assert_eq!( - ldap_handler.do_bind(&request).await, - request.gen_error( - LdapResultCode::NamingViolation, - r#"Unexpected user DN format. Got "cn=bob,dc=example,dc=com", expected: "cn=username,ou=people,dc=example,dc=com""#.to_string() - ) + ldap_handler.do_bind(&request).await.0, + LdapResultCode::NamingViolation, ); - let request = SimpleBindRequest { - msgid: 2, + let request = LdapBindRequest { dn: "cn=bob,ou=groups,dc=example,dc=com".to_string(), - pw: "pass".to_string(), + cred: LdapBindCred::Simple("pass".to_string()), }; assert_eq!( - ldap_handler.do_bind(&request).await, - request.gen_error( - LdapResultCode::NamingViolation, - r#"Unexpected user DN format. Got "cn=bob,ou=groups,dc=example,dc=com", expected: "cn=username,ou=people,dc=example,dc=com""# - .to_string() - ) + ldap_handler.do_bind(&request).await.0, + LdapResultCode::NamingViolation, ); } @@ -702,25 +725,14 @@ mod tests { ]) }); let mut ldap_handler = setup_bound_handler(mock).await; - let request = SearchRequest { - msgid: 2, - base: "ou=people,dc=example,dc=com".to_string(), - scope: LdapSearchScope::Base, - filter: LdapFilter::And(vec![]), - attrs: vec![ - "objectClass".to_string(), - "dn".to_string(), - "uid".to_string(), - "mail".to_string(), - "givenName".to_string(), - "sn".to_string(), - "cn".to_string(), - ], - }; + let request = make_search_request( + LdapFilter::And(vec![]), + vec!["objectClass", "dn", "uid", "mail", "givenName", "sn", "cn"], + ); assert_eq!( ldap_handler.do_search(&request).await, vec![ - request.gen_result_entry(LdapSearchResultEntry { + LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "cn=bob_1,ou=people,dc=example,dc=com".to_string(), attributes: vec![ LdapPartialAttribute { @@ -758,7 +770,7 @@ mod tests { } ], }), - request.gen_result_entry(LdapSearchResultEntry { + LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "cn=jim,ou=people,dc=example,dc=com".to_string(), attributes: vec![ LdapPartialAttribute { @@ -796,7 +808,7 @@ mod tests { } ], }), - request.gen_success() + make_search_success(), ] ); } @@ -814,37 +826,31 @@ mod tests { .times(1) .return_once(|_| Ok(vec![])); let mut ldap_handler = setup_bound_handler(mock).await; - let request = SearchRequest { - msgid: 2, - base: "ou=people,dc=example,dc=com".to_string(), - scope: LdapSearchScope::Base, - filter: LdapFilter::And(vec![LdapFilter::Or(vec![LdapFilter::Not(Box::new( + let request = make_search_request( + LdapFilter::And(vec![LdapFilter::Or(vec![LdapFilter::Not(Box::new( LdapFilter::Equality("uid".to_string(), "bob".to_string()), ))])]), - attrs: vec!["objectClass".to_string()], - }; + vec!["objectClass"], + ); assert_eq!( ldap_handler.do_search(&request).await, - vec![request.gen_success()] + vec![make_search_success()] ); } #[tokio::test] async fn test_search_unsupported_filters() { let mut ldap_handler = setup_bound_handler(MockTestBackendHandler::new()).await; - let request = SearchRequest { - msgid: 2, - base: "ou=people,dc=example,dc=com".to_string(), - scope: LdapSearchScope::Base, - filter: LdapFilter::Substring( + let request = make_search_request( + LdapFilter::Substring( "uid".to_string(), ldap3_server::proto::LdapSubstringFilter::default(), ), - attrs: vec!["objectClass".to_string()], - }; + vec!["objectClass"], + ); assert_eq!( ldap_handler.do_search(&request).await, - vec![request.gen_error( + vec![make_search_error( LdapResultCode::UnwillingToPerform, "Unsupported user filter: Unsupported user filter: Substring(\"uid\", LdapSubstringFilter { initial: None, any: [], final_: None })".to_string() )] diff --git a/server/src/infra/ldap_server.rs b/server/src/infra/ldap_server.rs index f30364e..0f0e3b5 100644 --- a/server/src/infra/ldap_server.rs +++ b/server/src/infra/ldap_server.rs @@ -4,10 +4,9 @@ use crate::infra::ldap_handler::LdapHandler; use actix_rt::net::TcpStream; use actix_server::ServerBuilder; use actix_service::{fn_service, ServiceFactoryExt}; -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use futures_util::future::ok; -use ldap3_server::simple::*; -use ldap3_server::LdapCodec; +use ldap3_server::{proto::LdapMsg, LdapCodec}; use log::*; use tokio::net::tcp::WriteHalf; use tokio_util::codec::{FramedRead, FramedWrite}; @@ -21,29 +20,19 @@ where Backend: BackendHandler + LoginHandler, { use futures_util::SinkExt; - use std::convert::TryFrom; - let server_op = match msg - .map_err(|e| warn!("Error while receiving LDAP op: {:#}", e)) - .and_then(ServerOps::try_from) - { - Ok(a_value) => a_value, - Err(an_error) => { - let _err = resp - .send(DisconnectionNotice::gen( - LdapResultCode::Other, - "Internal Server Error", - )) - .await; - let _err = resp.flush().await; - bail!("Internal server error: {:?}", an_error); - } - }; - - match session.handle_ldap_message(server_op).await { + let msg = msg.map_err(|e| anyhow!("Error while receiving LDAP op: {:#}", e))?; + match session.handle_ldap_message(msg.op).await { None => return Ok(false), Some(result) => { - for rmsg in result.into_iter() { - if let Err(e) = resp.send(rmsg).await { + for result_op in result.into_iter() { + if let Err(e) = resp + .send(LdapMsg { + msgid: msg.msgid, + op: result_op, + ctrl: vec![], + }) + .await + { bail!("Error while sending a response: {:?}", e); } }