server: split off password handling from ldap_handler

This commit is contained in:
Valentin Tolmer
2025-04-04 17:51:17 -05:00
committed by nitnelave
parent 63f8b51c88
commit 37a85b4c2e
2 changed files with 428 additions and 380 deletions
+28 -361
View File
@@ -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<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
})]
}
async fn change_password<B: OpaqueHandler>(
&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<Vec<LdapOp>> {
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<LdapOp> {
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<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
});
}
if let [value] = &change.modification.vals.as_slice() {
self.change_password(self.get_opaque_handler(), user_id, value)
password::change_password(self.get_opaque_handler(), user_id, value)
.await
.map_err(|e| LdapError {
code: LdapResultCode::Other,
@@ -733,54 +654,6 @@ pub mod tests {
setup_bound_handler_with_group(mock, "lldap_admin").await
}
#[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_whoami_empty() {
let mut ldap_handler =
@@ -809,212 +682,6 @@ pub mod tests {
);
}
#[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(),
)])
);
}
#[tokio::test]
async fn test_create_user() {
let mut mock = MockTestBackendHandler::new();
+400 -19
View File
@@ -1,10 +1,23 @@
use crate::domain::ldap::{
error::{LdapError, LdapResult},
utils::{LdapInfo, get_user_id_from_distinguished_name},
use crate::{
domain::{
ldap::{
error::{LdapError, LdapResult},
utils::{LdapInfo, get_user_id_from_distinguished_name},
},
opaque_handler::OpaqueHandler,
},
infra::{
access_control::{AccessControlledBackendHandler, UserReadableBackendHandler},
ldap::handler::make_extended_response,
},
};
use ldap3_proto::proto::{LdapBindCred, LdapBindRequest, LdapResultCode};
use anyhow::Result;
use ldap3_proto::proto::{
LdapBindCred, LdapBindRequest, LdapOp, LdapPasswordModifyRequest, LdapResultCode,
};
use lldap_auth::access_control::ValidationResults;
use lldap_domain::types::UserId;
use lldap_domain_handlers::handler::{BindRequest, LoginHandler};
use lldap_domain_handlers::handler::{BackendHandler, BindRequest, LoginHandler};
pub(crate) async fn do_bind(
ldap_info: &LdapInfo,
@@ -53,13 +66,116 @@ pub(crate) async fn do_bind(
}
}
pub(crate) async fn change_password<B: OpaqueHandler>(
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<Handler: BackendHandler>(
credentials: &ValidationResults,
ldap_info: &LdapInfo,
backend_handler: &AccessControlledBackendHandler<Handler>,
opaque_handler: &impl OpaqueHandler,
request: &LdapPasswordModifyRequest,
) -> LdapResult<Vec<LdapOp>> {
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(),
)])
);
}
}