From d423c64d57bcffc90474c2c4b782e624d675040a Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Sun, 24 Oct 2021 18:03:09 +0900 Subject: [PATCH 1/5] ldap: Switch to using LdapOp instead of ServerOp This is in preparation of supporting the password change message, since this is from the Extended Operations that is not available in the simple ServerOp. --- server/src/infra/ldap_handler.rs | 336 ++++++++++++++++--------------- server/src/infra/ldap_server.rs | 37 ++-- 2 files changed, 184 insertions(+), 189 deletions(-) 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); } } From 63f4bf95d2d657f44d42bf39d8266a7e36b00d40 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Sun, 24 Oct 2021 18:28:44 +0900 Subject: [PATCH 2/5] build: Enable linking with lld --- config.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 config.toml diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..f4d28a3 --- /dev/null +++ b/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["-C", "link-arg=-fuse-ld=lld"] From 026a2f7eb0932ba0ba3aae66950a43635b083595 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 29 Oct 2021 00:13:34 +0900 Subject: [PATCH 3/5] app: Fix the login button not re-enabling after failed login --- app/src/components/login.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/components/login.rs b/app/src/components/login.rs index f66349a..0bbfe80 100644 --- a/app/src/components/login.rs +++ b/app/src/components/login.rs @@ -78,6 +78,7 @@ impl LoginForm { // simple one to the user. ConsoleService::error(&format!("Invalid username or password: {}", e)); self.error = Some(anyhow!("Invalid username or password")); + self.task = None; return Ok(true); } Ok(l) => l, From 31e1ff358b0c7335799e33cb497337de17c8b91b Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 29 Oct 2021 01:03:26 +0900 Subject: [PATCH 4/5] ldap: Implement a rootDSE response This is the message that broadcasts the capabilities of the server, including the supported extensions. --- server/src/infra/ldap_handler.rs | 82 +++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 26a67f9..a3cd5a3 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -106,7 +106,6 @@ fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result Ok(vec![user.last_name.clone()]), "cn" => Ok(vec![user.display_name.clone()]), "displayName" => Ok(vec![user.display_name.clone()]), - "supportedExtension" => Ok(vec![]), _ => bail!("Unsupported user attribute: {}", attribute), } } @@ -140,7 +139,6 @@ fn get_group_attribute(group: &Group, base_dn_str: &str, attribute: &str) -> Res .iter() .map(|u| format!("cn={},ou=people,{}", u, base_dn_str)) .collect()), - "supportedExtension" => Ok(vec![]), _ => bail!("Unsupported group attribute: {}", attribute), } } @@ -210,6 +208,38 @@ fn make_search_error(code: LdapResultCode, message: String) -> LdapOp { }) } +fn root_dse_response(base_dn: &str) -> LdapOp { + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "".to_string(), + attributes: vec![ + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec!["top".to_string()], + }, + LdapPartialAttribute { + atype: "vendorName".to_string(), + vals: vec!["LLDAP".to_string()], + }, + LdapPartialAttribute { + atype: "vendorVersion".to_string(), + vals: vec!["lldap_0.2.0".to_string()], + }, + LdapPartialAttribute { + atype: "supportedLDAPVersion".to_string(), + vals: vec!["3".to_string()], + }, + LdapPartialAttribute { + atype: "supportedExtension".to_string(), + vals: vec!["1.3.6.1.4.1.4203.1.11.1".to_string()], + }, + LdapPartialAttribute { + atype: "defaultnamingcontext".to_string(), + vals: vec![base_dn.to_string()], + }, + ], + }) +} + pub struct LdapHandler { dn: String, backend_handler: Backend, @@ -275,17 +305,21 @@ impl LdapHandler { ), )]; } - let dn_parts = if request.base.is_empty() { - self.base_dn.clone() - } else { - match parse_distinguished_name(&request.base) { - Ok(dn) => dn, - Err(_) => { - return vec![make_search_error( - LdapResultCode::OperationsError, - format!(r#"Could not parse base DN: "{}""#, request.base), - )] - } + if request.base.is_empty() + && request.scope == LdapSearchScope::Base + && request.filter == LdapFilter::Present("objectClass".to_string()) + { + info!("Received rootDSE request"); + return vec![root_dse_response(&self.base_dn_str), make_search_success()]; + } + info!("Received search request: {:?}", &request); + let dn_parts = match parse_distinguished_name(&request.base) { + Ok(dn) => dn, + Err(_) => { + return vec![make_search_error( + LdapResultCode::OperationsError, + format!(r#"Could not parse base DN: "{}""#, request.base), + )] } }; if !is_subtree(&dn_parts, &self.base_dn) { @@ -856,4 +890,26 @@ mod tests { )] ); } + + #[tokio::test] + async fn test_search_root_dse() { + let mut ldap_handler = setup_bound_handler(MockTestBackendHandler::new()).await; + let request = LdapSearchRequest { + base: "".to_string(), + scope: LdapSearchScope::Base, + aliases: LdapDerefAliases::Never, + sizelimit: 0, + timelimit: 0, + typesonly: false, + filter: LdapFilter::Present("objectClass".to_string()), + attrs: vec!["supportedExtension".to_string()], + }; + assert_eq!( + ldap_handler.do_search(&request).await, + vec![ + root_dse_response("dc=example,dc=com"), + make_search_success() + ] + ); + } } From 43ffeca24dd8fea6eaae0936e56c444341d8b88e Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 29 Oct 2021 01:04:09 +0900 Subject: [PATCH 5/5] ldap: Add support for password modify extension This allows other systems (e.g. Authelia) to reset passwords for users. --- Cargo.lock | 4 +- server/Cargo.toml | 2 +- server/src/domain/opaque_handler.rs | 17 +- server/src/infra/ldap_handler.rs | 309 +++++++++++++++++++++++++--- server/src/infra/ldap_server.rs | 14 +- 5 files changed, 306 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea79431..c22eff0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1644,9 +1644,9 @@ dependencies = [ [[package]] name = "ldap3_server" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beb05c22d6cb1792389efb3e71ed90af6148b6f26d283db67322d356ab2556d" +checksum = "092da326ef499380e33fc8213a621de7fb342d6cd112eb695e16161a0acb061a" dependencies = [ "bytes", "lber", diff --git a/server/Cargo.toml b/server/Cargo.toml index 17a1361..8106869 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -26,7 +26,7 @@ futures-util = "*" hmac = "0.10" http = "*" jwt = "0.13" -ldap3_server = "*" +ldap3_server = ">=0.1.9" lldap_auth = { path = "../auth" } log = "*" orion = "0.16" diff --git a/server/src/domain/opaque_handler.rs b/server/src/domain/opaque_handler.rs index 0e524e0..dcdf083 100644 --- a/server/src/domain/opaque_handler.rs +++ b/server/src/domain/opaque_handler.rs @@ -28,9 +28,18 @@ mockall::mock! { } #[async_trait] impl OpaqueHandler for TestOpaqueHandler { - async fn login_start(&self, request: login::ClientLoginStartRequest) -> Result; - async fn login_finish(&self, request: login::ClientLoginFinishRequest ) -> Result; - async fn registration_start(&self, request: registration::ClientRegistrationStartRequest) -> Result; - async fn registration_finish(&self, request: registration::ClientRegistrationFinishRequest ) -> Result<()>; + async fn login_start( + &self, + request: login::ClientLoginStartRequest + ) -> Result; + async fn login_finish(&self, request: login::ClientLoginFinishRequest ) -> Result; + async fn registration_start( + &self, + request: registration::ClientRegistrationStartRequest + ) -> Result; + async fn registration_finish( + &self, + request: registration::ClientRegistrationFinishRequest + ) -> Result<()>; } } diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index a3cd5a3..65017f4 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -1,14 +1,19 @@ -use crate::domain::handler::{ - BackendHandler, BindRequest, Group, GroupIdAndName, LoginHandler, RequestFilter, User, +use crate::domain::{ + handler::{ + BackendHandler, BindRequest, Group, GroupIdAndName, LoginHandler, RequestFilter, User, + }, + opaque_handler::OpaqueHandler, }; use anyhow::{bail, Result}; use futures::stream::StreamExt; use futures_util::TryStreamExt; use ldap3_server::proto::{ - LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedResponse, LdapFilter, LdapOp, - LdapPartialAttribute, LdapResult, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, + LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedRequest, LdapExtendedResponse, + LdapFilter, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, LdapResult, + LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, LdapSearchScope, }; use log::*; +use std::convert::TryFrom; fn make_dn_pair(mut iter: I) -> Result<(String, String)> where @@ -133,8 +138,12 @@ fn make_ldap_search_user_result_entry( fn get_group_attribute(group: &Group, base_dn_str: &str, attribute: &str) -> Result> { match attribute { "objectClass" => Ok(vec!["groupOfUniqueNames".to_string()]), + "dn" => Ok(vec![format!( + "cn={},ou=groups,{}", + group.display_name, base_dn_str + )]), "cn" => Ok(vec![group.display_name.clone()]), - "uniqueMember" => Ok(group + "member" | "uniqueMember" => Ok(group .users .iter() .map(|u| format!("cn={},ou=people,{}", u, base_dn_str)) @@ -208,6 +217,19 @@ fn make_search_error(code: LdapResultCode, message: String) -> LdapOp { }) } +fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp { + LdapOp::ExtendedResponse(LdapExtendedResponse { + res: LdapResult { + code, + matcheddn: "".to_string(), + message, + referral: vec![], + }, + name: None, + value: None, + }) +} + fn root_dse_response(base_dn: &str) -> LdapOp { LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "".to_string(), @@ -240,7 +262,7 @@ fn root_dse_response(base_dn: &str) -> LdapOp { }) } -pub struct LdapHandler { +pub struct LdapHandler { dn: String, backend_handler: Backend, pub base_dn: Vec<(String, String)>, @@ -248,7 +270,7 @@ pub struct LdapHandler { ldap_user_dn: String, } -impl LdapHandler { +impl LdapHandler { pub fn new(backend_handler: Backend, ldap_base_dn: String, ldap_user_dn: String) -> Self { Self { dn: "Unauthenticated".to_string(), @@ -291,11 +313,73 @@ impl LdapHandler { } } + async fn change_password(&mut self, user: &str, password: &str) -> Result<()> { + use lldap_auth::*; + let mut rng = rand::rngs::OsRng; + let registration_start_request = + opaque::client::registration::start_registration(password, &mut rng)?; + let req = registration::ClientRegistrationStartRequest { + username: user.to_string(), + registration_start_request: registration_start_request.message, + }; + let registration_start_response = self.backend_handler.registration_start(req).await?; + let registration_finish = opaque::client::registration::finish_registration( + registration_start_request.state, + registration_start_response.registration_response, + &mut rng, + )?; + let req = registration::ClientRegistrationFinishRequest { + server_data: registration_start_response.server_data, + registration_upload: registration_finish.message, + }; + self.backend_handler.registration_finish(req).await?; + Ok(()) + } + + async fn do_password_modification( + &mut self, + request: &LdapPasswordModifyRequest, + ) -> Vec { + match (&request.user_identity, &request.new_password) { + (Some(user), Some(password)) => { + match get_user_id_from_distinguished_name(user, &self.base_dn, &self.base_dn_str) { + Ok(uid) => { + if let Err(e) = self.change_password(&uid, password).await { + vec![make_extended_response( + LdapResultCode::Other, + format!("Error while changing the password: {:#?}", e), + )] + } else { + vec![make_extended_response( + LdapResultCode::Success, + "".to_string(), + )] + } + } + Err(e) => vec![make_extended_response( + LdapResultCode::InvalidDNSyntax, + format!("Invalid username: {} ({:#?})", user, e), + )], + } + } + _ => vec![make_extended_response( + LdapResultCode::ConstraintViolation, + "Missing either user_id or password".to_string(), + )], + } + } + + async fn do_extended_request(&mut self, request: &LdapExtendedRequest) -> Vec { + match LdapPasswordModifyRequest::try_from(request) { + Ok(password_request) => self.do_password_modification(&password_request).await, + Err(_) => vec![make_extended_response( + LdapResultCode::UnwillingToPerform, + format!("Unsupported extended operation: {}", &request.name), + )], + } + } + 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![make_search_error( LdapResultCode::InsufficentAccessRights, @@ -474,16 +558,11 @@ impl LdapHandler { // No need to notify on unbind (per rfc4511) return None; } - op => vec![LdapOp::ExtendedResponse(LdapExtendedResponse { - res: LdapResult { - code: LdapResultCode::UnwillingToPerform, - matcheddn: "".to_string(), - message: format!("Unsupported operation: {:#?}", op), - referral: vec![], - }, - name: None, - value: None, - })], + LdapOp::ExtendedRequest(request) => self.do_extended_request(&request).await, + op => vec![make_extended_response( + LdapResultCode::UnwillingToPerform, + format!("Unsupported operation: {:#?}", op), + )], }) } @@ -503,6 +582,7 @@ impl LdapHandler { bail!("Unsupported group filter: {:?}", filter) } } + LdapFilter::And(v) if v.is_empty() => Ok(None), _ => bail!("Unsupported group filter: {:?}", filter), } } @@ -562,17 +642,63 @@ impl LdapHandler { #[cfg(test)] mod tests { use super::*; - use crate::domain::handler::{BindRequest, MockTestBackendHandler}; + use crate::domain::{error::Result, handler::*, opaque_handler::*}; + use async_trait::async_trait; use ldap3_server::proto::{LdapDerefAliases, LdapSearchScope}; use mockall::predicate::eq; + use std::collections::HashSet; use tokio; + mockall::mock! { + pub TestBackendHandler{} + impl Clone for TestBackendHandler { + fn clone(&self) -> Self; + } + #[async_trait] + impl LoginHandler for TestBackendHandler { + async fn bind(&self, request: BindRequest) -> Result<()>; + } + #[async_trait] + impl BackendHandler for TestBackendHandler { + async fn list_users(&self, filters: Option) -> Result>; + async fn list_groups(&self) -> Result>; + async fn get_user_details(&self, user_id: &str) -> Result; + async fn get_group_details(&self, group_id: GroupId) -> Result; + async fn get_user_groups(&self, user: &str) -> Result>; + async fn create_user(&self, request: CreateUserRequest) -> Result<()>; + async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; + async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; + async fn delete_user(&self, user_id: &str) -> Result<()>; + async fn create_group(&self, group_name: &str) -> Result; + async fn delete_group(&self, group_id: GroupId) -> Result<()>; + async fn add_user_to_group(&self, user_id: &str, group_id: GroupId) -> Result<()>; + async fn remove_user_from_group(&self, user_id: &str, group_id: GroupId) -> Result<()>; + } + #[async_trait] + impl OpaqueHandler for TestBackendHandler { + async fn login_start( + &self, + request: login::ClientLoginStartRequest + ) -> Result; + async fn login_finish(&self, request: login::ClientLoginFinishRequest) -> Result; + async fn registration_start( + &self, + request: registration::ClientRegistrationStartRequest + ) -> Result; + async fn registration_finish( + &self, + request: registration::ClientRegistrationFinishRequest + ) -> Result<()>; + } + } + fn make_search_request>( + base: &str, filter: LdapFilter, attrs: Vec, ) -> LdapSearchRequest { LdapSearchRequest { - base: "ou=people,dc=example,dc=com".to_string(), + base: base.to_string(), scope: LdapSearchScope::Base, aliases: LdapDerefAliases::Never, sizelimit: 0, @@ -583,6 +709,13 @@ mod tests { } } + fn make_user_search_request>( + filter: LdapFilter, + attrs: Vec, + ) -> LdapSearchRequest { + make_search_request::("ou=people,dc=example,dc=com", filter, attrs) + } + async fn setup_bound_handler( mut mock: MockTestBackendHandler, ) -> LdapHandler { @@ -673,7 +806,7 @@ mod tests { LdapResultCode::Success ); - let request = make_search_request::(LdapFilter::And(vec![]), vec![]); + let request = make_user_search_request::(LdapFilter::And(vec![]), vec![]); assert_eq!( ldap_handler.do_search(&request).await, vec![make_search_error( @@ -736,7 +869,7 @@ mod tests { } #[tokio::test] - async fn test_search() { + async fn test_search_users() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users().times(1).return_once(|_| { Ok(vec![ @@ -759,7 +892,7 @@ mod tests { ]) }); let mut ldap_handler = setup_bound_handler(mock).await; - let request = make_search_request( + let request = make_user_search_request( LdapFilter::And(vec![]), vec!["objectClass", "dn", "uid", "mail", "givenName", "sn", "cn"], ); @@ -847,6 +980,126 @@ mod tests { ); } + #[tokio::test] + async fn test_search_groups() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups().times(1).return_once(|| { + Ok(vec![ + Group { + id: GroupId(1), + display_name: "group_1".to_string(), + users: vec!["bob".to_string(), "john".to_string()], + }, + Group { + id: GroupId(3), + display_name: "bestgroup".to_string(), + users: vec!["john".to_string()], + }, + ]) + }); + let mut ldap_handler = setup_bound_handler(mock).await; + let request = make_search_request( + "ou=groups,dc=example,dc=com", + LdapFilter::And(vec![]), + vec!["objectClass", "dn", "cn", "uniqueMember"], + ); + assert_eq!( + ldap_handler.do_search(&request).await, + vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), + attributes: vec![ + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec!["groupOfUniqueNames".to_string(),] + }, + LdapPartialAttribute { + atype: "dn".to_string(), + vals: vec!["cn=group_1,ou=groups,dc=example,dc=com".to_string()] + }, + LdapPartialAttribute { + atype: "cn".to_string(), + vals: vec!["group_1".to_string()] + }, + LdapPartialAttribute { + atype: "uniqueMember".to_string(), + vals: vec![ + "cn=bob,ou=people,dc=example,dc=com".to_string(), + "cn=john,ou=people,dc=example,dc=com".to_string(), + ] + }, + ], + }), + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "cn=bestgroup,ou=groups,dc=example,dc=com".to_string(), + attributes: vec![ + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec!["groupOfUniqueNames".to_string(),] + }, + LdapPartialAttribute { + atype: "dn".to_string(), + vals: vec!["cn=bestgroup,ou=groups,dc=example,dc=com".to_string()] + }, + LdapPartialAttribute { + atype: "cn".to_string(), + vals: vec!["bestgroup".to_string()] + }, + LdapPartialAttribute { + atype: "uniqueMember".to_string(), + vals: vec!["cn=john,ou=people,dc=example,dc=com".to_string()] + }, + ], + }), + make_search_success(), + ] + ); + } + + #[tokio::test] + async fn test_search_groups_filter() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_groups() + .with(eq("bob")) + .times(1) + .return_once(|_| { + let mut set = HashSet::new(); + set.insert(GroupIdAndName(GroupId(1), "group_1".to_string())); + Ok(set) + }); + mock.expect_list_users() + .with(eq(Some(RequestFilter::MemberOfId(GroupId(1))))) + .times(1) + .return_once(|_| { + Ok(vec![User { + user_id: "bob".to_string(), + ..Default::default() + }]) + }); + let mut ldap_handler = setup_bound_handler(mock).await; + let request = make_search_request( + "ou=groups,dc=example,dc=com", + LdapFilter::Equality( + "uniqueMember".to_string(), + "cn=bob,ou=people,dc=example,dc=com".to_string(), + ), + vec!["cn"], + ); + assert_eq!( + ldap_handler.do_search(&request).await, + vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), + attributes: vec![LdapPartialAttribute { + atype: "cn".to_string(), + vals: vec!["group_1".to_string()] + },], + }), + make_search_success(), + ] + ); + } + #[tokio::test] async fn test_search_filters() { let mut mock = MockTestBackendHandler::new(); @@ -860,7 +1113,7 @@ mod tests { .times(1) .return_once(|_| Ok(vec![])); let mut ldap_handler = setup_bound_handler(mock).await; - let request = make_search_request( + let request = make_user_search_request( LdapFilter::And(vec![LdapFilter::Or(vec![LdapFilter::Not(Box::new( LdapFilter::Equality("uid".to_string(), "bob".to_string()), ))])]), @@ -875,7 +1128,7 @@ mod tests { #[tokio::test] async fn test_search_unsupported_filters() { let mut ldap_handler = setup_bound_handler(MockTestBackendHandler::new()).await; - let request = make_search_request( + let request = make_user_search_request( LdapFilter::Substring( "uid".to_string(), ldap3_server::proto::LdapSubstringFilter::default(), diff --git a/server/src/infra/ldap_server.rs b/server/src/infra/ldap_server.rs index 0f0e3b5..8ea0800 100644 --- a/server/src/infra/ldap_server.rs +++ b/server/src/infra/ldap_server.rs @@ -1,6 +1,10 @@ -use crate::domain::handler::{BackendHandler, LoginHandler}; -use crate::infra::configuration::Configuration; -use crate::infra::ldap_handler::LdapHandler; +use crate::{ + domain::{ + handler::{BackendHandler, LoginHandler}, + opaque_handler::OpaqueHandler, + }, + infra::{configuration::Configuration, ldap_handler::LdapHandler}, +}; use actix_rt::net::TcpStream; use actix_server::ServerBuilder; use actix_service::{fn_service, ServiceFactoryExt}; @@ -17,7 +21,7 @@ async fn handle_incoming_message( session: &mut LdapHandler, ) -> Result where - Backend: BackendHandler + LoginHandler, + Backend: BackendHandler + LoginHandler + OpaqueHandler, { use futures_util::SinkExt; let msg = msg.map_err(|e| anyhow!("Error while receiving LDAP op: {:#}", e))?; @@ -51,7 +55,7 @@ pub fn build_ldap_server( server_builder: ServerBuilder, ) -> Result where - Backend: BackendHandler + LoginHandler + 'static, + Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static, { use futures_util::StreamExt;