diff --git a/server/src/infra/ldap/handler.rs b/server/src/infra/ldap/handler.rs index 16a8db6..29a37b3 100644 --- a/server/src/infra/ldap/handler.rs +++ b/server/src/infra/ldap/handler.rs @@ -14,13 +14,15 @@ use crate::{ access_control::{ AccessControlledBackendHandler, AdminBackendHandler, UserReadableBackendHandler, }, - ldap::search::{ - self, is_root_dse_request, make_search_error, make_search_request, make_search_success, - root_dse_response, + ldap::{ + password::{self, do_password_modification}, + search::{ + self, is_root_dse_request, make_search_error, make_search_request, + make_search_success, root_dse_response, + }, }, }, }; -use anyhow::Result; use ldap3_proto::proto::{ LdapAddRequest, LdapAttribute, LdapBindRequest, LdapBindResponse, LdapCompareRequest, LdapExtendedRequest, LdapExtendedResponse, LdapFilter, LdapModify, LdapModifyRequest, @@ -36,8 +38,6 @@ use lldap_domain_handlers::handler::{BackendHandler, LoginHandler}; use std::collections::HashMap; use tracing::{debug, instrument}; -use super::password; - fn make_add_error(code: LdapResultCode, message: String) -> LdapOp { LdapOp::AddResponse(LdapResultOp { code, @@ -47,7 +47,7 @@ fn make_add_error(code: LdapResultCode, message: String) -> LdapOp { }) } -fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp { +pub(crate) fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp { LdapOp::ExtendedResponse(LdapExtendedResponse { res: LdapResultOp { code, @@ -60,7 +60,7 @@ fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp { }) } -fn make_modify_response(code: LdapResultCode, message: String) -> LdapOp { +pub(crate) fn make_modify_response(code: LdapResultCode, message: String) -> LdapOp { LdapOp::ModifyResponse(LdapResultOp { code, matcheddn: "".to_string(), @@ -184,109 +184,30 @@ impl LdapHandler( - &self, - backend_handler: &B, - user: UserId, - password: &[u8], - ) -> 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.clone(), - registration_start_request: registration_start_request.message, - }; - let registration_start_response = 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, - }; - backend_handler.registration_finish(req).await?; - Ok(()) - } - - async fn do_password_modification( - &mut self, - request: &LdapPasswordModifyRequest, - ) -> LdapResult> { - let credentials = self.user_info.as_ref().ok_or_else(|| LdapError { - code: LdapResultCode::InsufficentAccessRights, - message: "No user currently bound".to_string(), - })?; - match (&request.user_identity, &request.new_password) { - (Some(user), Some(password)) => { - match get_user_id_from_distinguished_name( - &user.to_ascii_lowercase(), - &self.ldap_info.base_dn, - &self.ldap_info.base_dn_str, - ) { - Ok(uid) => { - let user_is_admin = self - .backend_handler - .get_readable_handler(credentials, &uid) - .expect("Unexpected permission error") - .get_user_groups(&uid) - .await - .map_err(|e| LdapError { - code: LdapResultCode::OperationsError, - message: format!( - "Internal error while requesting user's groups: {:#?}", - e - ), - })? - .iter() - .any(|g| g.display_name == "lldap_admin".into()); - if !credentials.can_change_password(&uid, user_is_admin) { - Err(LdapError { - code: LdapResultCode::InsufficentAccessRights, - message: format!( - r#"User `{}` cannot modify the password of user `{}`"#, - &credentials.user, &uid - ), - }) - } else if let Err(e) = self - .change_password(self.get_opaque_handler(), uid, password.as_bytes()) - .await - { - Err(LdapError { - code: LdapResultCode::Other, - message: format!("Error while changing the password: {:#?}", e), - }) - } else { - Ok(vec![make_extended_response( - LdapResultCode::Success, - "".to_string(), - )]) - } - } - Err(e) => Err(LdapError { - code: LdapResultCode::InvalidDNSyntax, - message: format!("Invalid username: {}", e), - }), - } - } - _ => Err(LdapError { - code: LdapResultCode::ConstraintViolation, - message: "Missing either user_id or password".to_string(), - }), - } - } - #[instrument(skip_all, level = "debug")] async fn do_extended_request(&mut self, request: &LdapExtendedRequest) -> Vec { match request.name.as_str() { OID_PASSWORD_MODIFY => match LdapPasswordModifyRequest::try_from(request) { - Ok(password_request) => self - .do_password_modification(&password_request) + Ok(password_request) => { + let credentials = match self.user_info.as_ref() { + Some(user_id) => user_id, + None => { + return vec![make_extended_response( + LdapResultCode::InsufficentAccessRights, + "No user currently bound".to_string(), + )]; + } + }; + do_password_modification( + credentials, + &self.ldap_info, + &self.backend_handler, + self.get_opaque_handler(), + &password_request, + ) .await - .unwrap_or_else(|e: LdapError| vec![make_extended_response(e.code, e.message)]), + .unwrap_or_else(|e: LdapError| vec![make_extended_response(e.code, e.message)]) + } Err(e) => vec![make_extended_response( LdapResultCode::ProtocolError, format!("Error while parsing password modify request: {:#?}", e), @@ -344,7 +265,7 @@ impl LdapHandler( + backend_handler: &B, + user: UserId, + password: &[u8], +) -> 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.clone(), + registration_start_request: registration_start_request.message, + }; + let registration_start_response = 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, + }; + backend_handler.registration_finish(req).await?; + Ok(()) +} + +pub(crate) async fn do_password_modification( + credentials: &ValidationResults, + ldap_info: &LdapInfo, + backend_handler: &AccessControlledBackendHandler, + opaque_handler: &impl OpaqueHandler, + request: &LdapPasswordModifyRequest, +) -> LdapResult> { + match (&request.user_identity, &request.new_password) { + (Some(user), Some(password)) => { + match get_user_id_from_distinguished_name( + &user.to_ascii_lowercase(), + &ldap_info.base_dn, + &ldap_info.base_dn_str, + ) { + Ok(uid) => { + let user_is_admin = backend_handler + .get_readable_handler(credentials, &uid) + .expect("Unexpected permission error") + .get_user_groups(&uid) + .await + .map_err(|e| LdapError { + code: LdapResultCode::OperationsError, + message: format!( + "Internal error while requesting user's groups: {:#?}", + e + ), + })? + .iter() + .any(|g| g.display_name == "lldap_admin".into()); + if !credentials.can_change_password(&uid, user_is_admin) { + Err(LdapError { + code: LdapResultCode::InsufficentAccessRights, + message: format!( + r#"User `{}` cannot modify the password of user `{}`"#, + &credentials.user, &uid + ), + }) + } else if let Err(e) = + change_password(opaque_handler, uid, password.as_bytes()).await + { + Err(LdapError { + code: LdapResultCode::Other, + message: format!("Error while changing the password: {:#?}", e), + }) + } else { + Ok(vec![make_extended_response( + LdapResultCode::Success, + "".to_string(), + )]) + } + } + Err(e) => Err(LdapError { + code: LdapResultCode::InvalidDNSyntax, + message: format!("Invalid username: {}", e), + }), + } + } + _ => Err(LdapError { + code: LdapResultCode::ConstraintViolation, + message: "Missing either user_id or password".to_string(), + }), + } +} + #[cfg(test)] pub mod tests { use super::*; - use crate::infra::ldap::handler::LdapHandler; - use crate::infra::test_utils::MockTestBackendHandler; + use crate::infra::{ + ldap::handler::{ + LdapHandler, make_modify_response, + tests::{ + setup_bound_admin_handler, setup_bound_password_manager_handler, + setup_bound_readonly_handler, + }, + }, + test_utils::MockTestBackendHandler, + }; use chrono::TimeZone; - use ldap3_proto::proto::{LdapBindResponse, LdapOp, LdapResult as LdapResultOp}; + use ldap3_proto::proto::{ + LdapBindResponse, LdapModify, LdapModifyRequest, LdapModifyType, LdapOp, + LdapResult as LdapResultOp, + }; + use ldap3_proto::{LdapPartialAttribute, proto::LdapExtendedRequest}; use lldap_domain::{types::*, uuid}; use mockall::predicate::eq; use pretty_assertions::assert_eq; @@ -102,10 +218,7 @@ pub mod tests { cred: LdapBindCred::Simple("pass".to_string()), }); assert_eq!( - ldap_handler - .handle_ldap_message(request) - .await - .unwrap(), + ldap_handler.handle_ldap_message(request).await.unwrap(), make_bind_success() ); } @@ -139,8 +252,7 @@ pub mod tests { dn: "uid=test,ou=people,dc=example,dc=com".to_string(), cred: LdapBindCred::Simple("pass".to_string()), }; - assert_eq!(ldap_handler.do_bind(&request).await, - make_bind_success()); + assert_eq!(ldap_handler.do_bind(&request).await, make_bind_success()); } #[tokio::test] @@ -154,7 +266,10 @@ pub mod tests { }; assert_eq!( ldap_handler.do_bind(&request).await, - make_bind_result(LdapResultCode::NamingViolation, r#"Unexpected DN format. Got "cn=bob,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#), + make_bind_result( + LdapResultCode::NamingViolation, + r#"Unexpected DN format. Got "cn=bob,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""# + ), ); let request = LdapBindRequest { dn: "uid=bob,dc=example,dc=com".to_string(), @@ -162,7 +277,10 @@ pub mod tests { }; assert_eq!( ldap_handler.do_bind(&request).await, - make_bind_result(LdapResultCode::NamingViolation, r#"Unexpected DN format. Got "uid=bob,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#), + make_bind_result( + LdapResultCode::NamingViolation, + r#"Unexpected DN format. Got "uid=bob,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""# + ), ); let request = LdapBindRequest { dn: "uid=bob,ou=groups,dc=example,dc=com".to_string(), @@ -170,7 +288,10 @@ pub mod tests { }; assert_eq!( ldap_handler.do_bind(&request).await, - make_bind_result(LdapResultCode::NamingViolation, r#"Unexpected DN format. Got "uid=bob,ou=groups,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#), + make_bind_result( + LdapResultCode::NamingViolation, + r#"Unexpected DN format. Got "uid=bob,ou=groups,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""# + ), ); let request = LdapBindRequest { dn: "uid=bob,ou=people,dc=example,dc=fr".to_string(), @@ -178,7 +299,10 @@ pub mod tests { }; assert_eq!( ldap_handler.do_bind(&request).await, - make_bind_result(LdapResultCode::NamingViolation, r#"Not a subtree of the base tree"#), + make_bind_result( + LdapResultCode::NamingViolation, + r#"Not a subtree of the base tree"# + ), ); let request = LdapBindRequest { dn: "uid=bob=test,ou=people,dc=example,dc=com".to_string(), @@ -186,7 +310,264 @@ pub mod tests { }; assert_eq!( ldap_handler.do_bind(&request).await, - make_bind_result(LdapResultCode::NamingViolation, r#"Too many elements in distinguished name: "uid", "bob", "test""#), + make_bind_result( + LdapResultCode::NamingViolation, + r#"Too many elements in distinguished name: "uid", "bob", "test""# + ), + ); + } + + #[tokio::test] + async fn test_password_change() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_groups() + .with(eq(UserId::new("bob"))) + .returning(|_| Ok(HashSet::new())); + use lldap_auth::*; + let mut rng = rand::rngs::OsRng; + let registration_start_request = + opaque::client::registration::start_registration("password".as_bytes(), &mut rng) + .unwrap(); + let request = registration::ClientRegistrationStartRequest { + username: "bob".into(), + registration_start_request: registration_start_request.message, + }; + let start_response = opaque::server::registration::start_registration( + &opaque::server::ServerSetup::new(&mut rng), + request.registration_start_request, + &request.username, + ) + .unwrap(); + mock.expect_registration_start().times(1).return_once(|_| { + Ok(registration::ServerRegistrationStartResponse { + server_data: "".to_string(), + registration_response: start_response.message, + }) + }); + mock.expect_registration_finish() + .times(1) + .return_once(|_| Ok(())); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), + old_password: None, + new_password: Some("password".to_string()), + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::Success, + "".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_modify_request() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_groups() + .with(eq(UserId::new("bob"))) + .returning(|_| Ok(HashSet::new())); + use lldap_auth::*; + let mut rng = rand::rngs::OsRng; + let registration_start_request = + opaque::client::registration::start_registration("password".as_bytes(), &mut rng) + .unwrap(); + let request = registration::ClientRegistrationStartRequest { + username: "bob".into(), + registration_start_request: registration_start_request.message, + }; + let start_response = opaque::server::registration::start_registration( + &opaque::server::ServerSetup::new(&mut rng), + request.registration_start_request, + &request.username, + ) + .unwrap(); + mock.expect_registration_start().times(1).return_once(|_| { + Ok(registration::ServerRegistrationStartResponse { + server_data: "".to_string(), + registration_response: start_response.message, + }) + }); + mock.expect_registration_finish() + .times(1) + .return_once(|_| Ok(())); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::ModifyRequest(LdapModifyRequest { + dn: "uid=bob,ou=people,dc=example,dc=com".to_string(), + changes: vec![LdapModify { + operation: LdapModifyType::Replace, + modification: LdapPartialAttribute { + atype: "userPassword".to_owned(), + vals: vec!["password".as_bytes().to_vec()], + }, + }], + }); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_modify_response( + LdapResultCode::Success, + "".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_password_manager() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_groups() + .with(eq(UserId::new("bob"))) + .returning(|_| Ok(HashSet::new())); + use lldap_auth::*; + let mut rng = rand::rngs::OsRng; + let registration_start_request = + opaque::client::registration::start_registration("password".as_bytes(), &mut rng) + .unwrap(); + let request = registration::ClientRegistrationStartRequest { + username: "bob".into(), + registration_start_request: registration_start_request.message, + }; + let start_response = opaque::server::registration::start_registration( + &opaque::server::ServerSetup::new(&mut rng), + request.registration_start_request, + &request.username, + ) + .unwrap(); + mock.expect_registration_start().times(1).return_once(|_| { + Ok(registration::ServerRegistrationStartResponse { + server_data: "".to_string(), + registration_response: start_response.message, + }) + }); + mock.expect_registration_finish() + .times(1) + .return_once(|_| Ok(())); + let mut ldap_handler = setup_bound_password_manager_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), + old_password: None, + new_password: Some("password".to_string()), + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::Success, + "".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_errors() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_groups() + .with(eq(UserId::new("bob"))) + .returning(|_| Ok(HashSet::new())); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: None, + old_password: None, + new_password: None, + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::ConstraintViolation, + "Missing either user_id or password".to_string(), + )]) + ); + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=groups,ou=people,dc=example,dc=com".to_string()), + old_password: None, + new_password: Some("password".to_string()), + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::InvalidDNSyntax, + r#"Invalid username: Unexpected DN format. Got "uid=bob,ou=groups,ou=people,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#.to_string(), + )]) + ); + let request = LdapOp::ExtendedRequest(LdapExtendedRequest { + name: "test".to_string(), + value: None, + }); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::UnwillingToPerform, + "Unsupported extended operation: test".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_unauthorized_password_manager() { + let mut mock = MockTestBackendHandler::new(); + let mut groups = HashSet::new(); + groups.insert(GroupDetails { + group_id: GroupId(0), + display_name: "lldap_admin".into(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + attributes: Vec::new(), + }); + mock.expect_get_user_groups() + .with(eq(UserId::new("bob"))) + .times(1) + .return_once(|_| Ok(groups)); + let mut ldap_handler = setup_bound_password_manager_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), + old_password: Some("pass".to_string()), + new_password: Some("password".to_string()), + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::InsufficentAccessRights, + "User `test` cannot modify the password of user `bob`".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_unauthorized_readonly() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_groups() + .with(eq(UserId::new("bob"))) + .times(1) + .return_once(|_| Ok(HashSet::new())); + let mut ldap_handler = setup_bound_readonly_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), + old_password: Some("pass".to_string()), + new_password: Some("password".to_string()), + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::InsufficentAccessRights, + "User `test` cannot modify the password of user `bob`".to_string(), + )]) ); } }