server, ldap: Use group membership for admin status

This commit is contained in:
Valentin Tolmer 2022-05-08 20:12:06 +02:00 committed by nitnelave
parent 5c1db3cf4a
commit ebffc1c086
2 changed files with 84 additions and 35 deletions

View File

@ -286,20 +286,23 @@ fn root_dse_response(base_dn: &str) -> LdapOp {
}) })
} }
#[derive(Clone, Copy, PartialEq, Debug)]
enum Permission {
Admin,
Regular,
}
pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> { pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> {
dn: LdapDn, user_info: Option<(UserId, Permission)>,
user_id: UserId,
backend_handler: Backend, backend_handler: Backend,
pub base_dn: Vec<(String, String)>, pub base_dn: Vec<(String, String)>,
base_dn_str: String, base_dn_str: String,
ldap_user_dn: LdapDn,
} }
impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> { impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> {
pub fn new(backend_handler: Backend, ldap_base_dn: String, ldap_user_dn: UserId) -> Self { pub fn new(backend_handler: Backend, ldap_base_dn: String) -> Self {
Self { Self {
dn: LdapDn("unauthenticated".to_string()), user_info: None,
user_id: UserId::new("unauthenticated"),
backend_handler, backend_handler,
base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| { base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| {
panic!( panic!(
@ -307,7 +310,6 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
ldap_base_dn ldap_base_dn
) )
}), }),
ldap_user_dn: LdapDn(format!("uid={},ou=people,{}", ldap_user_dn, &ldap_base_dn)),
base_dn_str: ldap_base_dn, base_dn_str: ldap_base_dn,
} }
} }
@ -332,8 +334,20 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
.await .await
{ {
Ok(()) => { Ok(()) => {
self.dn = LdapDn(request.dn.clone()); let is_admin = self
self.user_id = user_id; .backend_handler
.get_user_groups(&user_id)
.await
.map(|groups| groups.iter().any(|g| g.1 == "lldap_admin"))
.unwrap_or(false);
self.user_info = Some((
user_id,
if is_admin {
Permission::Admin
} else {
Permission::Regular
},
));
(LdapResultCode::Success, "".to_string()) (LdapResultCode::Success, "".to_string())
} }
Err(_) => (LdapResultCode::InvalidCredentials, "".to_string()), Err(_) => (LdapResultCode::InvalidCredentials, "".to_string()),
@ -367,10 +381,28 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
&mut self, &mut self,
request: &LdapPasswordModifyRequest, request: &LdapPasswordModifyRequest,
) -> Vec<LdapOp> { ) -> Vec<LdapOp> {
let (user_id, permission) = match &self.user_info {
Some(info) => info,
_ => {
return vec![make_search_error(
LdapResultCode::InsufficentAccessRights,
"No user currently bound".to_string(),
)];
}
};
match (&request.user_identity, &request.new_password) { match (&request.user_identity, &request.new_password) {
(Some(user), Some(password)) => { (Some(user), Some(password)) => {
match get_user_id_from_distinguished_name(user, &self.base_dn, &self.base_dn_str) { match get_user_id_from_distinguished_name(user, &self.base_dn, &self.base_dn_str) {
Ok(uid) => { Ok(uid) => {
if *permission != Permission::Admin && user_id != &uid {
return vec![make_search_error(
LdapResultCode::InsufficentAccessRights,
format!(
r#"User {} cannot modify the password of user {}"#,
&user_id, &uid
),
)];
}
if let Err(e) = self.change_password(&uid, password).await { if let Err(e) = self.change_password(&uid, password).await {
vec![make_extended_response( vec![make_extended_response(
LdapResultCode::Other, LdapResultCode::Other,
@ -407,7 +439,16 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
pub async fn do_search(&mut self, request: &LdapSearchRequest) -> Vec<LdapOp> { pub async fn do_search(&mut self, request: &LdapSearchRequest) -> Vec<LdapOp> {
let admin = self.dn == self.ldap_user_dn; let user_filter = match &self.user_info {
Some((_, Permission::Admin)) => None,
Some((user_id, Permission::Regular)) => Some(user_id),
None => {
return vec![make_search_error(
LdapResultCode::InsufficentAccessRights,
"No user currently bound".to_string(),
)];
}
};
if request.base.is_empty() if request.base.is_empty()
&& request.scope == LdapSearchScope::Base && request.scope == LdapSearchScope::Base
&& request.filter == LdapFilter::Present("objectClass".to_string()) && request.filter == LdapFilter::Present("objectClass".to_string())
@ -435,7 +476,6 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
let mut results = Vec::new(); let mut results = Vec::new();
let mut got_match = false; let mut got_match = false;
let user_filter = if admin { None } else { Some(&self.user_id) };
if dn_parts.len() == self.base_dn.len() if dn_parts.len() == self.base_dn.len()
|| (dn_parts.len() == self.base_dn.len() + 1 || (dn_parts.len() == self.base_dn.len() + 1
&& dn_parts[0] == ("ou".to_string(), "people".to_string())) && dn_parts[0] == ("ou".to_string(), "people".to_string()))
@ -573,8 +613,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
LdapOp::SearchRequest(request) => self.do_search(&request).await, LdapOp::SearchRequest(request) => self.do_search(&request).await,
LdapOp::UnbindRequest => { LdapOp::UnbindRequest => {
self.dn = LdapDn("unauthenticated".to_string()); self.user_info = None;
self.user_id = UserId::new("unauthenticated");
// No need to notify on unbind (per rfc4511) // No need to notify on unbind (per rfc4511)
return None; return None;
} }
@ -779,8 +818,14 @@ mod tests {
password: "pass".to_string(), password: "pass".to_string(),
})) }))
.return_once(|_| Ok(())); .return_once(|_| Ok(()));
let mut ldap_handler = mock.expect_get_user_groups()
LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("test")); .with(eq(UserId::new("test")))
.return_once(|_| {
let mut set = HashSet::new();
set.insert(GroupIdAndName(GroupId(42), "lldap_admin".to_string()));
Ok(set)
});
let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string());
let request = LdapBindRequest { let request = LdapBindRequest {
dn: "uid=test,ou=people,dc=example,dc=com".to_string(), dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()), cred: LdapBindCred::Simple("pass".to_string()),
@ -802,8 +847,10 @@ mod tests {
})) }))
.times(1) .times(1)
.return_once(|_| Ok(())); .return_once(|_| Ok(()));
let mut ldap_handler = mock.expect_get_user_groups()
LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("test")); .with(eq(UserId::new("bob")))
.return_once(|_| Ok(HashSet::new()));
let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string());
let request = LdapOp::BindRequest(LdapBindRequest { let request = LdapOp::BindRequest(LdapBindRequest {
dn: "uid=bob,ou=people,dc=example,dc=com".to_string(), dn: "uid=bob,ou=people,dc=example,dc=com".to_string(),
@ -833,8 +880,14 @@ mod tests {
})) }))
.times(1) .times(1)
.return_once(|_| Ok(())); .return_once(|_| Ok(()));
let mut ldap_handler = mock.expect_get_user_groups()
LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("test")); .with(eq(UserId::new("test")))
.return_once(|_| {
let mut set = HashSet::new();
set.insert(GroupIdAndName(GroupId(42), "lldap_admin".to_string()));
Ok(set)
});
let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string());
let request = LdapBindRequest { let request = LdapBindRequest {
dn: "uid=test,ou=people,dc=example,dc=com".to_string(), dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
@ -868,8 +921,10 @@ mod tests {
..Default::default() ..Default::default()
}]) }])
}); });
let mut ldap_handler = mock.expect_get_user_groups()
LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("admin")); .with(eq(UserId::new("test")))
.return_once(|_| Ok(HashSet::new()));
let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string());
let request = LdapBindRequest { let request = LdapBindRequest {
dn: "uid=test,ou=people,dc=example,dc=com".to_string(), dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
@ -897,8 +952,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_bind_invalid_dn() { async fn test_bind_invalid_dn() {
let mock = MockTestBackendHandler::new(); let mock = MockTestBackendHandler::new();
let mut ldap_handler = let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string());
LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("admin"));
let request = LdapBindRequest { let request = LdapBindRequest {
dn: "cn=bob,dc=example,dc=com".to_string(), dn: "cn=bob,dc=example,dc=com".to_string(),

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
domain::{ domain::{
handler::{BackendHandler, LoginHandler, UserId}, handler::{BackendHandler, LoginHandler},
opaque_handler::OpaqueHandler, opaque_handler::OpaqueHandler,
}, },
infra::{configuration::Configuration, ldap_handler::LdapHandler}, infra::{configuration::Configuration, ldap_handler::LdapHandler},
@ -70,7 +70,6 @@ async fn handle_ldap_stream<Stream, Backend>(
stream: Stream, stream: Stream,
backend_handler: Backend, backend_handler: Backend,
ldap_base_dn: String, ldap_base_dn: String,
ldap_user_dn: UserId,
) -> Result<Stream> ) -> Result<Stream>
where where
Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static, Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static,
@ -82,7 +81,7 @@ where
let mut requests = FramedRead::new(r, LdapCodec); let mut requests = FramedRead::new(r, LdapCodec);
let mut resp = FramedWrite::new(w, LdapCodec); let mut resp = FramedWrite::new(w, LdapCodec);
let mut session = LdapHandler::new(backend_handler, ldap_base_dn, ldap_user_dn); let mut session = LdapHandler::new(backend_handler, ldap_base_dn);
while let Some(msg) = requests.next().await { while let Some(msg) = requests.next().await {
if !handle_incoming_message(msg, &mut resp, &mut session) if !handle_incoming_message(msg, &mut resp, &mut session)
@ -111,11 +110,7 @@ pub fn build_ldap_server<Backend>(
where where
Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static, Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static,
{ {
let context = ( let context = (backend_handler, config.ldap_base_dn.clone());
backend_handler,
config.ldap_base_dn.clone(),
config.ldap_user_dn.clone(),
);
let context_for_tls = context.clone(); let context_for_tls = context.clone();
@ -124,8 +119,8 @@ where
fn_service(move |stream: TcpStream| { fn_service(move |stream: TcpStream| {
let context = context.clone(); let context = context.clone();
async move { async move {
let (handler, base_dn, user_dn) = context; let (handler, base_dn) = context;
handle_ldap_stream(stream, handler, base_dn, user_dn).await handle_ldap_stream(stream, handler, base_dn).await
} }
}) })
.map_err(|err: anyhow::Error| error!("[LDAP] Service Error: {:#}", err)) .map_err(|err: anyhow::Error| error!("[LDAP] Service Error: {:#}", err))
@ -144,9 +139,9 @@ where
fn_service(move |stream: TcpStream| { fn_service(move |stream: TcpStream| {
let tls_context = tls_context.clone(); let tls_context = tls_context.clone();
async move { async move {
let ((handler, base_dn, user_dn), tls_acceptor) = tls_context; let ((handler, base_dn), tls_acceptor) = tls_context;
let tls_stream = tls_acceptor.accept(stream).await?; let tls_stream = tls_acceptor.accept(stream).await?;
handle_ldap_stream(tls_stream, handler, base_dn, user_dn).await handle_ldap_stream(tls_stream, handler, base_dn).await
} }
}) })
.map_err(|err: anyhow::Error| error!("[LDAPS] Service Error: {:#}", err)) .map_err(|err: anyhow::Error| error!("[LDAPS] Service Error: {:#}", err))