mirror of
https://github.com/lldap/lldap.git
synced 2026-03-31 15:07:48 +01:00
server: split off password handling from ldap_handler
This commit is contained in:
committed by
nitnelave
parent
63f8b51c88
commit
37a85b4c2e
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
)])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user