ldap-rp/src/op/bind.rs

148 lines
4.6 KiB
Rust

use futures_util::SinkExt;
use ldap3_proto::LdapCodec;
use ldap3_proto::control::*;
use ldap3_proto::proto::*;
use tokio::io::AsyncWrite;
use tokio_util::codec::FramedWrite;
use std::sync::Arc;
use crate::{BasicLdapClient, ClientState, Config, Dn, LdapError};
pub fn bind_operror(msgid: i32, msg: &str) -> LdapMsg {
LdapMsg {
msgid,
op: LdapOp::BindResponse(LdapBindResponse {
res: LdapResult {
code: LdapResultCode::OperationsError,
matcheddn: "".to_string(),
message: msg.to_string(),
referral: vec![],
},
saslcreds: None,
}),
ctrl: vec![],
}
}
pub async fn bind<W: AsyncWrite + Unpin>(
w: &mut FramedWrite<W, LdapCodec>,
mut lbr: LdapBindRequest,
config: Arc<Config>,
msgid: i32,
ctrl: Vec<LdapControl>,
) -> Result<Option<ClientState>, LdapError> {
trace!("{:?}", lbr);
if lbr.dn == "" {
// Here we pretend to have successfully bound so that
// a client performing an anonymous bind can proceed with
// more requests (such as a search request).
// This supports ldap search which always performs a bind.
let resp_msg = LdapMsg {
msgid,
op: LdapOp::BindResponse(LdapBindResponse {
res: LdapResult {
code: LdapResultCode::Success,
matcheddn: "".to_string(),
message: "".to_string(),
referral: vec![],
},
saslcreds: None,
}),
ctrl: vec![],
};
w.send(resp_msg).await.map_err(|err| {
error!("Unable to send response: {err}");
LdapError::Transport
})?;
// We still treat the client as unbounded because it doesn't
// have a session to a backend.
return Ok(Some(ClientState::Unbound));
}
let request_dn = lbr.dn.clone();
debug!("Received bind request on DN: {}", lbr.dn);
let mut dn = Dn::from_dn_str(&lbr.dn)?;
let Some(requested_domain) = dn.get_hostname() else {
debug!("No domain name CN found in DN: {}", lbr.dn);
return Err(LdapError::InvalidQuery);
};
// Lowercase the domain systematically to allow matches
let requested_domain = requested_domain.to_lowercase();
let Some(mapping) = config
.mapping
.iter()
.find(|x| x.from.to_lowercase() == requested_domain)
else {
// TODO: we should probably return an error to the client here
debug!("No mapping found for domain {requested_domain}");
return Err(LdapError::InvalidQuery);
};
debug!(
"Redirecting {} to {} with domain {}",
requested_domain, mapping.backend, mapping.to
);
dn.set_hostname(&mapping.to, false);
lbr.dn = dn.to_dn_string();
let backend_dn = lbr.dn.clone();
// We need the client to connect *and* bind to proceed here!
let mut client = match BasicLdapClient::build(&mapping.backend).await {
Ok(c) => c,
Err(e) => {
error!("A client build error has occurred: {e:?}");
let resp_msg = bind_operror(msgid, "unable to bind");
w.send(resp_msg).await.map_err(|err| {
error!("Unable to send response: {err}");
LdapError::Transport
})?;
// Always bail.
return Ok(None);
}
};
let valid = match client.bind(lbr, ctrl).await {
Ok((bind_resp, ctrl)) => {
// Almost there, lets check the bind result.
let valid = bind_resp.res.code == LdapResultCode::Success;
let resp_msg = LdapMsg {
msgid,
op: LdapOp::BindResponse(bind_resp),
ctrl,
};
w.send(resp_msg).await.map_err(|err| {
error!("Unable to send response: {err}");
LdapError::Transport
})?;
valid
}
Err(e) => {
error!("A client bind error has occurred: {e:?}");
let resp_msg = bind_operror(msgid, "unable to bind");
w.send(resp_msg).await.map_err(|err| {
error!("Unable to send response: {err}");
LdapError::Transport
})?;
// Always bail.
return Ok(None);
}
};
if valid {
info!("Successful bind for `{request_dn}` -> `{backend_dn}`");
Ok(Some(ClientState::Authenticated {
request_dn,
backend_dn,
client,
}))
} else {
Ok(None)
}
}