ldap: Add support for password modify extension

This allows other systems (e.g. Authelia) to reset passwords for users.
This commit is contained in:
Valentin Tolmer 2021-10-29 01:04:09 +09:00 committed by nitnelave
parent 31e1ff358b
commit 43ffeca24d
5 changed files with 306 additions and 40 deletions

4
Cargo.lock generated
View File

@ -1644,9 +1644,9 @@ dependencies = [
[[package]] [[package]]
name = "ldap3_server" name = "ldap3_server"
version = "0.1.7" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beb05c22d6cb1792389efb3e71ed90af6148b6f26d283db67322d356ab2556d" checksum = "092da326ef499380e33fc8213a621de7fb342d6cd112eb695e16161a0acb061a"
dependencies = [ dependencies = [
"bytes", "bytes",
"lber", "lber",

View File

@ -26,7 +26,7 @@ futures-util = "*"
hmac = "0.10" hmac = "0.10"
http = "*" http = "*"
jwt = "0.13" jwt = "0.13"
ldap3_server = "*" ldap3_server = ">=0.1.9"
lldap_auth = { path = "../auth" } lldap_auth = { path = "../auth" }
log = "*" log = "*"
orion = "0.16" orion = "0.16"

View File

@ -28,9 +28,18 @@ mockall::mock! {
} }
#[async_trait] #[async_trait]
impl OpaqueHandler for TestOpaqueHandler { impl OpaqueHandler for TestOpaqueHandler {
async fn login_start(&self, request: login::ClientLoginStartRequest) -> Result<login::ServerLoginStartResponse>; async fn login_start(
&self,
request: login::ClientLoginStartRequest
) -> Result<login::ServerLoginStartResponse>;
async fn login_finish(&self, request: login::ClientLoginFinishRequest ) -> Result<String>; async fn login_finish(&self, request: login::ClientLoginFinishRequest ) -> Result<String>;
async fn registration_start(&self, request: registration::ClientRegistrationStartRequest) -> Result<registration::ServerRegistrationStartResponse>; async fn registration_start(
async fn registration_finish(&self, request: registration::ClientRegistrationFinishRequest ) -> Result<()>; &self,
request: registration::ClientRegistrationStartRequest
) -> Result<registration::ServerRegistrationStartResponse>;
async fn registration_finish(
&self,
request: registration::ClientRegistrationFinishRequest
) -> Result<()>;
} }
} }

View File

@ -1,14 +1,19 @@
use crate::domain::handler::{ use crate::domain::{
handler::{
BackendHandler, BindRequest, Group, GroupIdAndName, LoginHandler, RequestFilter, User, BackendHandler, BindRequest, Group, GroupIdAndName, LoginHandler, RequestFilter, User,
},
opaque_handler::OpaqueHandler,
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use futures::stream::StreamExt; use futures::stream::StreamExt;
use futures_util::TryStreamExt; use futures_util::TryStreamExt;
use ldap3_server::proto::{ use ldap3_server::proto::{
LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedResponse, LdapFilter, LdapOp, LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedRequest, LdapExtendedResponse,
LdapPartialAttribute, LdapResult, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, LdapFilter, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, LdapResult,
LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, LdapSearchScope,
}; };
use log::*; use log::*;
use std::convert::TryFrom;
fn make_dn_pair<I>(mut iter: I) -> Result<(String, String)> fn make_dn_pair<I>(mut iter: I) -> Result<(String, String)>
where where
@ -133,8 +138,12 @@ fn make_ldap_search_user_result_entry(
fn get_group_attribute(group: &Group, base_dn_str: &str, attribute: &str) -> Result<Vec<String>> { fn get_group_attribute(group: &Group, base_dn_str: &str, attribute: &str) -> Result<Vec<String>> {
match attribute { match attribute {
"objectClass" => Ok(vec!["groupOfUniqueNames".to_string()]), "objectClass" => Ok(vec!["groupOfUniqueNames".to_string()]),
"dn" => Ok(vec![format!(
"cn={},ou=groups,{}",
group.display_name, base_dn_str
)]),
"cn" => Ok(vec![group.display_name.clone()]), "cn" => Ok(vec![group.display_name.clone()]),
"uniqueMember" => Ok(group "member" | "uniqueMember" => Ok(group
.users .users
.iter() .iter()
.map(|u| format!("cn={},ou=people,{}", u, base_dn_str)) .map(|u| format!("cn={},ou=people,{}", u, base_dn_str))
@ -208,6 +217,19 @@ fn make_search_error(code: LdapResultCode, message: String) -> LdapOp {
}) })
} }
fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp {
LdapOp::ExtendedResponse(LdapExtendedResponse {
res: LdapResult {
code,
matcheddn: "".to_string(),
message,
referral: vec![],
},
name: None,
value: None,
})
}
fn root_dse_response(base_dn: &str) -> LdapOp { fn root_dse_response(base_dn: &str) -> LdapOp {
LdapOp::SearchResultEntry(LdapSearchResultEntry { LdapOp::SearchResultEntry(LdapSearchResultEntry {
dn: "".to_string(), dn: "".to_string(),
@ -240,7 +262,7 @@ fn root_dse_response(base_dn: &str) -> LdapOp {
}) })
} }
pub struct LdapHandler<Backend: BackendHandler + LoginHandler> { pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> {
dn: String, dn: String,
backend_handler: Backend, backend_handler: Backend,
pub base_dn: Vec<(String, String)>, pub base_dn: Vec<(String, String)>,
@ -248,7 +270,7 @@ pub struct LdapHandler<Backend: BackendHandler + LoginHandler> {
ldap_user_dn: String, ldap_user_dn: String,
} }
impl<Backend: BackendHandler + LoginHandler> LdapHandler<Backend> { impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> {
pub fn new(backend_handler: Backend, ldap_base_dn: String, ldap_user_dn: String) -> Self { pub fn new(backend_handler: Backend, ldap_base_dn: String, ldap_user_dn: String) -> Self {
Self { Self {
dn: "Unauthenticated".to_string(), dn: "Unauthenticated".to_string(),
@ -291,11 +313,73 @@ impl<Backend: BackendHandler + LoginHandler> LdapHandler<Backend> {
} }
} }
async fn change_password(&mut self, user: &str, password: &str) -> Result<()> {
use lldap_auth::*;
let mut rng = rand::rngs::OsRng;
let registration_start_request =
opaque::client::registration::start_registration(password, &mut rng)?;
let req = registration::ClientRegistrationStartRequest {
username: user.to_string(),
registration_start_request: registration_start_request.message,
};
let registration_start_response = self.backend_handler.registration_start(req).await?;
let registration_finish = opaque::client::registration::finish_registration(
registration_start_request.state,
registration_start_response.registration_response,
&mut rng,
)?;
let req = registration::ClientRegistrationFinishRequest {
server_data: registration_start_response.server_data,
registration_upload: registration_finish.message,
};
self.backend_handler.registration_finish(req).await?;
Ok(())
}
async fn do_password_modification(
&mut self,
request: &LdapPasswordModifyRequest,
) -> Vec<LdapOp> {
match (&request.user_identity, &request.new_password) {
(Some(user), Some(password)) => {
match get_user_id_from_distinguished_name(user, &self.base_dn, &self.base_dn_str) {
Ok(uid) => {
if let Err(e) = self.change_password(&uid, password).await {
vec![make_extended_response(
LdapResultCode::Other,
format!("Error while changing the password: {:#?}", e),
)]
} else {
vec![make_extended_response(
LdapResultCode::Success,
"".to_string(),
)]
}
}
Err(e) => vec![make_extended_response(
LdapResultCode::InvalidDNSyntax,
format!("Invalid username: {} ({:#?})", user, e),
)],
}
}
_ => vec![make_extended_response(
LdapResultCode::ConstraintViolation,
"Missing either user_id or password".to_string(),
)],
}
}
async fn do_extended_request(&mut self, request: &LdapExtendedRequest) -> Vec<LdapOp> {
match LdapPasswordModifyRequest::try_from(request) {
Ok(password_request) => self.do_password_modification(&password_request).await,
Err(_) => vec![make_extended_response(
LdapResultCode::UnwillingToPerform,
format!("Unsupported extended operation: {}", &request.name),
)],
}
}
pub async fn do_search(&mut self, request: &LdapSearchRequest) -> Vec<LdapOp> { pub async fn do_search(&mut self, request: &LdapSearchRequest) -> Vec<LdapOp> {
info!(
"Received search request with filters: {:?}",
&request.filter
);
if self.dn != self.ldap_user_dn { if self.dn != self.ldap_user_dn {
return vec![make_search_error( return vec![make_search_error(
LdapResultCode::InsufficentAccessRights, LdapResultCode::InsufficentAccessRights,
@ -474,16 +558,11 @@ impl<Backend: BackendHandler + LoginHandler> LdapHandler<Backend> {
// No need to notify on unbind (per rfc4511) // No need to notify on unbind (per rfc4511)
return None; return None;
} }
op => vec![LdapOp::ExtendedResponse(LdapExtendedResponse { LdapOp::ExtendedRequest(request) => self.do_extended_request(&request).await,
res: LdapResult { op => vec![make_extended_response(
code: LdapResultCode::UnwillingToPerform, LdapResultCode::UnwillingToPerform,
matcheddn: "".to_string(), format!("Unsupported operation: {:#?}", op),
message: format!("Unsupported operation: {:#?}", op), )],
referral: vec![],
},
name: None,
value: None,
})],
}) })
} }
@ -503,6 +582,7 @@ impl<Backend: BackendHandler + LoginHandler> LdapHandler<Backend> {
bail!("Unsupported group filter: {:?}", filter) bail!("Unsupported group filter: {:?}", filter)
} }
} }
LdapFilter::And(v) if v.is_empty() => Ok(None),
_ => bail!("Unsupported group filter: {:?}", filter), _ => bail!("Unsupported group filter: {:?}", filter),
} }
} }
@ -562,17 +642,63 @@ impl<Backend: BackendHandler + LoginHandler> LdapHandler<Backend> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::domain::handler::{BindRequest, MockTestBackendHandler}; use crate::domain::{error::Result, handler::*, opaque_handler::*};
use async_trait::async_trait;
use ldap3_server::proto::{LdapDerefAliases, LdapSearchScope}; use ldap3_server::proto::{LdapDerefAliases, LdapSearchScope};
use mockall::predicate::eq; use mockall::predicate::eq;
use std::collections::HashSet;
use tokio; use tokio;
mockall::mock! {
pub TestBackendHandler{}
impl Clone for TestBackendHandler {
fn clone(&self) -> Self;
}
#[async_trait]
impl LoginHandler for TestBackendHandler {
async fn bind(&self, request: BindRequest) -> Result<()>;
}
#[async_trait]
impl BackendHandler for TestBackendHandler {
async fn list_users(&self, filters: Option<RequestFilter>) -> Result<Vec<User>>;
async fn list_groups(&self) -> Result<Vec<Group>>;
async fn get_user_details(&self, user_id: &str) -> Result<User>;
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupIdAndName>;
async fn get_user_groups(&self, user: &str) -> Result<HashSet<GroupIdAndName>>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
async fn delete_user(&self, user_id: &str) -> Result<()>;
async fn create_group(&self, group_name: &str) -> Result<GroupId>;
async fn delete_group(&self, group_id: GroupId) -> Result<()>;
async fn add_user_to_group(&self, user_id: &str, group_id: GroupId) -> Result<()>;
async fn remove_user_from_group(&self, user_id: &str, group_id: GroupId) -> Result<()>;
}
#[async_trait]
impl OpaqueHandler for TestBackendHandler {
async fn login_start(
&self,
request: login::ClientLoginStartRequest
) -> Result<login::ServerLoginStartResponse>;
async fn login_finish(&self, request: login::ClientLoginFinishRequest) -> Result<String>;
async fn registration_start(
&self,
request: registration::ClientRegistrationStartRequest
) -> Result<registration::ServerRegistrationStartResponse>;
async fn registration_finish(
&self,
request: registration::ClientRegistrationFinishRequest
) -> Result<()>;
}
}
fn make_search_request<S: Into<String>>( fn make_search_request<S: Into<String>>(
base: &str,
filter: LdapFilter, filter: LdapFilter,
attrs: Vec<S>, attrs: Vec<S>,
) -> LdapSearchRequest { ) -> LdapSearchRequest {
LdapSearchRequest { LdapSearchRequest {
base: "ou=people,dc=example,dc=com".to_string(), base: base.to_string(),
scope: LdapSearchScope::Base, scope: LdapSearchScope::Base,
aliases: LdapDerefAliases::Never, aliases: LdapDerefAliases::Never,
sizelimit: 0, sizelimit: 0,
@ -583,6 +709,13 @@ mod tests {
} }
} }
fn make_user_search_request<S: Into<String>>(
filter: LdapFilter,
attrs: Vec<S>,
) -> LdapSearchRequest {
make_search_request::<S>("ou=people,dc=example,dc=com", filter, attrs)
}
async fn setup_bound_handler( async fn setup_bound_handler(
mut mock: MockTestBackendHandler, mut mock: MockTestBackendHandler,
) -> LdapHandler<MockTestBackendHandler> { ) -> LdapHandler<MockTestBackendHandler> {
@ -673,7 +806,7 @@ mod tests {
LdapResultCode::Success LdapResultCode::Success
); );
let request = make_search_request::<String>(LdapFilter::And(vec![]), vec![]); let request = make_user_search_request::<String>(LdapFilter::And(vec![]), vec![]);
assert_eq!( assert_eq!(
ldap_handler.do_search(&request).await, ldap_handler.do_search(&request).await,
vec![make_search_error( vec![make_search_error(
@ -736,7 +869,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn test_search() { async fn test_search_users() {
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();
mock.expect_list_users().times(1).return_once(|_| { mock.expect_list_users().times(1).return_once(|_| {
Ok(vec![ Ok(vec![
@ -759,7 +892,7 @@ mod tests {
]) ])
}); });
let mut ldap_handler = setup_bound_handler(mock).await; let mut ldap_handler = setup_bound_handler(mock).await;
let request = make_search_request( let request = make_user_search_request(
LdapFilter::And(vec![]), LdapFilter::And(vec![]),
vec!["objectClass", "dn", "uid", "mail", "givenName", "sn", "cn"], vec!["objectClass", "dn", "uid", "mail", "givenName", "sn", "cn"],
); );
@ -847,6 +980,126 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_search_groups() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_groups().times(1).return_once(|| {
Ok(vec![
Group {
id: GroupId(1),
display_name: "group_1".to_string(),
users: vec!["bob".to_string(), "john".to_string()],
},
Group {
id: GroupId(3),
display_name: "bestgroup".to_string(),
users: vec!["john".to_string()],
},
])
});
let mut ldap_handler = setup_bound_handler(mock).await;
let request = make_search_request(
"ou=groups,dc=example,dc=com",
LdapFilter::And(vec![]),
vec!["objectClass", "dn", "cn", "uniqueMember"],
);
assert_eq!(
ldap_handler.do_search(&request).await,
vec![
LdapOp::SearchResultEntry(LdapSearchResultEntry {
dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["groupOfUniqueNames".to_string(),]
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["cn=group_1,ou=groups,dc=example,dc=com".to_string()]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["group_1".to_string()]
},
LdapPartialAttribute {
atype: "uniqueMember".to_string(),
vals: vec![
"cn=bob,ou=people,dc=example,dc=com".to_string(),
"cn=john,ou=people,dc=example,dc=com".to_string(),
]
},
],
}),
LdapOp::SearchResultEntry(LdapSearchResultEntry {
dn: "cn=bestgroup,ou=groups,dc=example,dc=com".to_string(),
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["groupOfUniqueNames".to_string(),]
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["cn=bestgroup,ou=groups,dc=example,dc=com".to_string()]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["bestgroup".to_string()]
},
LdapPartialAttribute {
atype: "uniqueMember".to_string(),
vals: vec!["cn=john,ou=people,dc=example,dc=com".to_string()]
},
],
}),
make_search_success(),
]
);
}
#[tokio::test]
async fn test_search_groups_filter() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_groups()
.with(eq("bob"))
.times(1)
.return_once(|_| {
let mut set = HashSet::new();
set.insert(GroupIdAndName(GroupId(1), "group_1".to_string()));
Ok(set)
});
mock.expect_list_users()
.with(eq(Some(RequestFilter::MemberOfId(GroupId(1)))))
.times(1)
.return_once(|_| {
Ok(vec![User {
user_id: "bob".to_string(),
..Default::default()
}])
});
let mut ldap_handler = setup_bound_handler(mock).await;
let request = make_search_request(
"ou=groups,dc=example,dc=com",
LdapFilter::Equality(
"uniqueMember".to_string(),
"cn=bob,ou=people,dc=example,dc=com".to_string(),
),
vec!["cn"],
);
assert_eq!(
ldap_handler.do_search(&request).await,
vec![
LdapOp::SearchResultEntry(LdapSearchResultEntry {
dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
attributes: vec![LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["group_1".to_string()]
},],
}),
make_search_success(),
]
);
}
#[tokio::test] #[tokio::test]
async fn test_search_filters() { async fn test_search_filters() {
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();
@ -860,7 +1113,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| Ok(vec![])); .return_once(|_| Ok(vec![]));
let mut ldap_handler = setup_bound_handler(mock).await; let mut ldap_handler = setup_bound_handler(mock).await;
let request = make_search_request( let request = make_user_search_request(
LdapFilter::And(vec![LdapFilter::Or(vec![LdapFilter::Not(Box::new( LdapFilter::And(vec![LdapFilter::Or(vec![LdapFilter::Not(Box::new(
LdapFilter::Equality("uid".to_string(), "bob".to_string()), LdapFilter::Equality("uid".to_string(), "bob".to_string()),
))])]), ))])]),
@ -875,7 +1128,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_search_unsupported_filters() { async fn test_search_unsupported_filters() {
let mut ldap_handler = setup_bound_handler(MockTestBackendHandler::new()).await; let mut ldap_handler = setup_bound_handler(MockTestBackendHandler::new()).await;
let request = make_search_request( let request = make_user_search_request(
LdapFilter::Substring( LdapFilter::Substring(
"uid".to_string(), "uid".to_string(),
ldap3_server::proto::LdapSubstringFilter::default(), ldap3_server::proto::LdapSubstringFilter::default(),

View File

@ -1,6 +1,10 @@
use crate::domain::handler::{BackendHandler, LoginHandler}; use crate::{
use crate::infra::configuration::Configuration; domain::{
use crate::infra::ldap_handler::LdapHandler; handler::{BackendHandler, LoginHandler},
opaque_handler::OpaqueHandler,
},
infra::{configuration::Configuration, ldap_handler::LdapHandler},
};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_server::ServerBuilder; use actix_server::ServerBuilder;
use actix_service::{fn_service, ServiceFactoryExt}; use actix_service::{fn_service, ServiceFactoryExt};
@ -17,7 +21,7 @@ async fn handle_incoming_message<Backend>(
session: &mut LdapHandler<Backend>, session: &mut LdapHandler<Backend>,
) -> Result<bool> ) -> Result<bool>
where where
Backend: BackendHandler + LoginHandler, Backend: BackendHandler + LoginHandler + OpaqueHandler,
{ {
use futures_util::SinkExt; use futures_util::SinkExt;
let msg = msg.map_err(|e| anyhow!("Error while receiving LDAP op: {:#}", e))?; let msg = msg.map_err(|e| anyhow!("Error while receiving LDAP op: {:#}", e))?;
@ -51,7 +55,7 @@ pub fn build_ldap_server<Backend>(
server_builder: ServerBuilder, server_builder: ServerBuilder,
) -> Result<ServerBuilder> ) -> Result<ServerBuilder>
where where
Backend: BackendHandler + LoginHandler + 'static, Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static,
{ {
use futures_util::StreamExt; use futures_util::StreamExt;