mirror of
https://github.com/lldap/lldap.git
synced 2026-06-21 05:23:37 +00:00
Add a modifyTimestamp attribute to LDAP entries for users and groups, and expose pwdChangedTime for users. These attributes let clients track when an entry (or its password) was last changed. - modifyTimestamp is a server-maintained attribute that updates on any write to user or group entries, including membership changes (on the group side). - pwdChangedTime is set when a user’s password is created or changed.
307 lines
11 KiB
Rust
307 lines
11 KiB
Rust
use crate::{
|
|
core::{
|
|
error::{LdapError, LdapResult},
|
|
utils::{LdapInfo, get_user_id_from_distinguished_name},
|
|
},
|
|
handler::make_modify_response,
|
|
password::{self},
|
|
};
|
|
use ldap3_proto::proto::{LdapModify, LdapModifyRequest, LdapModifyType, LdapOp, LdapResultCode};
|
|
use lldap_access_control::UserReadableBackendHandler;
|
|
use lldap_auth::access_control::ValidationResults;
|
|
use lldap_domain::types::UserId;
|
|
use lldap_opaque_handler::OpaqueHandler;
|
|
|
|
async fn handle_modify_change(
|
|
opaque_handler: &impl OpaqueHandler,
|
|
user_id: UserId,
|
|
credentials: &ValidationResults,
|
|
user_is_admin: bool,
|
|
change: &LdapModify,
|
|
) -> LdapResult<()> {
|
|
if !change
|
|
.modification
|
|
.atype
|
|
.eq_ignore_ascii_case("userpassword")
|
|
|| change.operation != LdapModifyType::Replace
|
|
{
|
|
return Err(LdapError {
|
|
code: LdapResultCode::UnwillingToPerform,
|
|
message: format!(
|
|
r#"Unsupported operation: `{:?}` for `{}`"#,
|
|
change.operation, change.modification.atype
|
|
),
|
|
});
|
|
}
|
|
if !credentials.can_change_password(&user_id, user_is_admin) {
|
|
return Err(LdapError {
|
|
code: LdapResultCode::InsufficentAccessRights,
|
|
message: format!(
|
|
r#"User `{}` cannot modify the password of user `{}`"#,
|
|
&credentials.user, &user_id
|
|
),
|
|
});
|
|
}
|
|
if let [value] = &change.modification.vals.as_slice() {
|
|
password::change_password(opaque_handler, user_id, value)
|
|
.await
|
|
.map_err(|e| LdapError {
|
|
code: LdapResultCode::Other,
|
|
message: format!("Error while changing the password: {e:#?}"),
|
|
})?;
|
|
} else {
|
|
return Err(LdapError {
|
|
code: LdapResultCode::InvalidAttributeSyntax,
|
|
message: format!(
|
|
r#"Wrong number of values for password attribute: {}"#,
|
|
change.modification.vals.len()
|
|
),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn handle_modify_request<'cred, UserBackendHandler>(
|
|
opaque_handler: &impl OpaqueHandler,
|
|
get_readable_handler: impl FnOnce(
|
|
&'cred ValidationResults,
|
|
UserId,
|
|
) -> Option<&'cred UserBackendHandler>,
|
|
ldap_info: &LdapInfo,
|
|
credentials: &'cred ValidationResults,
|
|
request: &LdapModifyRequest,
|
|
) -> LdapResult<Vec<LdapOp>>
|
|
where
|
|
// Note: ideally, get_readable_handler would take UserId by reference, but I couldn't make the lifetimes work.
|
|
UserBackendHandler: UserReadableBackendHandler + 'cred,
|
|
{
|
|
match get_user_id_from_distinguished_name(
|
|
&request.dn,
|
|
&ldap_info.base_dn,
|
|
&ldap_info.base_dn_str,
|
|
) {
|
|
Ok(uid) => {
|
|
let user_is_admin = get_readable_handler(credentials, uid.clone())
|
|
.ok_or_else(|| LdapError {
|
|
code: LdapResultCode::InsufficentAccessRights,
|
|
message: format!(
|
|
"User `{}` cannot modify user `{}`",
|
|
credentials.user.as_str(),
|
|
uid.as_str()
|
|
),
|
|
})?
|
|
.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());
|
|
for change in &request.changes {
|
|
handle_modify_change(
|
|
opaque_handler,
|
|
uid.clone(),
|
|
credentials,
|
|
user_is_admin,
|
|
change,
|
|
)
|
|
.await?
|
|
}
|
|
Ok(vec![make_modify_response(
|
|
LdapResultCode::Success,
|
|
String::new(),
|
|
)])
|
|
}
|
|
Err(e) => Err(LdapError {
|
|
code: LdapResultCode::InvalidDNSyntax,
|
|
message: format!("Invalid username: {e}"),
|
|
}),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
handler::tests::{
|
|
setup_bound_admin_handler, setup_bound_handler_with_group,
|
|
setup_bound_password_manager_handler,
|
|
},
|
|
password::tests::expect_password_change,
|
|
};
|
|
use chrono::TimeZone;
|
|
use ldap3_proto::proto::LdapResult as LdapResultOp;
|
|
use lldap_domain::{
|
|
types::{GroupDetails, GroupId, GroupName, UserId},
|
|
uuid,
|
|
};
|
|
use lldap_test_utils::MockTestBackendHandler;
|
|
use mockall::predicate::eq;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::HashSet;
|
|
|
|
fn setup_target_user_groups(
|
|
mock: &mut MockTestBackendHandler,
|
|
target_user: &str,
|
|
groups: Vec<&'static str>,
|
|
) {
|
|
mock.expect_get_user_groups()
|
|
.times(1)
|
|
.with(eq(UserId::from(target_user)))
|
|
.return_once(move |_| {
|
|
let mut g = HashSet::<GroupDetails>::new();
|
|
for group in groups {
|
|
g.insert(GroupDetails {
|
|
group_id: GroupId(42),
|
|
display_name: GroupName::from(group),
|
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
|
attributes: Vec::new(),
|
|
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
});
|
|
}
|
|
Ok(g)
|
|
});
|
|
}
|
|
|
|
fn make_password_modify_request(target_user: &str) -> LdapModifyRequest {
|
|
LdapModifyRequest {
|
|
dn: format!("uid={target_user},ou=people,dc=example,dc=com"),
|
|
changes: vec![LdapModify {
|
|
operation: LdapModifyType::Replace,
|
|
modification: ldap3_proto::LdapPartialAttribute {
|
|
atype: "userPassword".to_string(),
|
|
vals: vec![b"tommy".to_vec()],
|
|
},
|
|
}],
|
|
}
|
|
}
|
|
|
|
fn make_modify_success_response() -> Vec<LdapOp> {
|
|
vec![LdapOp::ModifyResponse(LdapResultOp {
|
|
code: LdapResultCode::Success,
|
|
matcheddn: "".to_string(),
|
|
message: "".to_string(),
|
|
referral: vec![],
|
|
})]
|
|
}
|
|
|
|
fn make_modify_failure_response(code: LdapResultCode, message: &str) -> Vec<LdapOp> {
|
|
vec![LdapOp::ModifyResponse(LdapResultOp {
|
|
code,
|
|
matcheddn: "".to_string(),
|
|
message: message.to_string(),
|
|
referral: vec![],
|
|
})]
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_password_of_regular_as_admin() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
setup_target_user_groups(&mut mock, "bob", Vec::new());
|
|
expect_password_change(&mut mock, "bob");
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_password_modify_request("bob");
|
|
assert_eq!(
|
|
ldap_handler.do_modify_request(&request).await,
|
|
make_modify_success_response()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_password_of_regular_as_regular() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
setup_target_user_groups(&mut mock, "test", Vec::new());
|
|
expect_password_change(&mut mock, "test");
|
|
let ldap_handler = setup_bound_handler_with_group(mock, "regular").await;
|
|
let request = make_password_modify_request("test");
|
|
assert_eq!(
|
|
ldap_handler.do_modify_request(&request).await,
|
|
make_modify_success_response()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_password_of_regular_as_password_manager() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
setup_target_user_groups(&mut mock, "bob", Vec::new());
|
|
expect_password_change(&mut mock, "bob");
|
|
let ldap_handler = setup_bound_password_manager_handler(mock).await;
|
|
let request = make_password_modify_request("bob");
|
|
assert_eq!(
|
|
ldap_handler.do_modify_request(&request).await,
|
|
make_modify_success_response()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_password_of_admin_as_password_manager() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
setup_target_user_groups(&mut mock, "bob", vec!["lldap_admin"]);
|
|
let ldap_handler = setup_bound_password_manager_handler(mock).await;
|
|
let request = make_password_modify_request("bob");
|
|
assert_eq!(
|
|
ldap_handler.do_modify_request(&request).await,
|
|
make_modify_failure_response(
|
|
LdapResultCode::InsufficentAccessRights,
|
|
"User `test` cannot modify the password of user `bob`"
|
|
)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_password_of_other_regular_as_regular() {
|
|
let ldap_handler =
|
|
setup_bound_handler_with_group(MockTestBackendHandler::new(), "regular").await;
|
|
let request = make_password_modify_request("bob");
|
|
assert_eq!(
|
|
ldap_handler.do_modify_request(&request).await,
|
|
make_modify_failure_response(
|
|
LdapResultCode::InsufficentAccessRights,
|
|
"User `test` cannot modify user `bob`"
|
|
)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_password_of_admin_as_admin() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
setup_target_user_groups(&mut mock, "test", vec!["lldap_admin"]);
|
|
expect_password_change(&mut mock, "test");
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_password_modify_request("test");
|
|
assert_eq!(
|
|
ldap_handler.do_modify_request(&request).await,
|
|
make_modify_success_response()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_password_invalid_number_of_values() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
setup_target_user_groups(&mut mock, "bob", Vec::new());
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = {
|
|
let target_user = "bob";
|
|
LdapModifyRequest {
|
|
dn: format!("uid={target_user},ou=people,dc=example,dc=com"),
|
|
changes: vec![LdapModify {
|
|
operation: LdapModifyType::Replace,
|
|
modification: ldap3_proto::LdapPartialAttribute {
|
|
atype: "userPassword".to_string(),
|
|
vals: vec![b"tommy".to_vec(), b"other_value".to_vec()],
|
|
},
|
|
}],
|
|
}
|
|
};
|
|
assert_eq!(
|
|
ldap_handler.do_modify_request(&request).await,
|
|
make_modify_failure_response(
|
|
LdapResultCode::InvalidAttributeSyntax,
|
|
"Wrong number of values for password attribute: 2"
|
|
)
|
|
);
|
|
}
|
|
}
|