server: allow non-admin user to do limited searches

This commit is contained in:
Valentin Tolmer 2022-04-25 09:03:55 +02:00 committed by nitnelave
parent 3ac38bb96f
commit f52197e76f

View File

@ -13,6 +13,9 @@ use ldap3_server::proto::{
}; };
use log::{debug, warn}; use log::{debug, warn};
#[derive(Debug, PartialEq, Eq, Clone)]
struct LdapDn(String);
fn make_dn_pair<I>(mut iter: I) -> Result<(String, String)> fn make_dn_pair<I>(mut iter: I) -> Result<(String, String)>
where where
I: Iterator<Item = String>, I: Iterator<Item = String>,
@ -94,24 +97,24 @@ fn get_user_id_from_distinguished_name(
} }
} }
fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result<Vec<String>> { fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result<Option<Vec<String>>> {
match attribute.to_lowercase().as_str() { Ok(Some(match attribute.to_lowercase().as_str() {
"objectclass" => Ok(vec![ "objectclass" => vec![
"inetOrgPerson".to_string(), "inetOrgPerson".to_string(),
"posixAccount".to_string(), "posixAccount".to_string(),
"mailAccount".to_string(), "mailAccount".to_string(),
"person".to_string(), "person".to_string(),
]), ],
"dn" => Ok(vec![dn.to_string()]), "dn" => vec![dn.to_string()],
"uid" => Ok(vec![user.user_id.to_string()]), "uid" => vec![user.user_id.to_string()],
"mail" => Ok(vec![user.email.clone()]), "mail" => vec![user.email.clone()],
"givenname" => Ok(vec![user.first_name.clone()]), "givenname" => vec![user.first_name.clone()],
"sn" => Ok(vec![user.last_name.clone()]), "sn" => vec![user.last_name.clone()],
"cn" | "displayname" => Ok(vec![user.display_name.clone()]), "cn" | "displayname" => vec![user.display_name.clone()],
"createtimestamp" | "modifytimestamp" => Ok(vec![user.creation_date.to_rfc3339()]), "createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()],
"1.1" => Ok(vec![]), "1.1" => return Ok(None),
_ => bail!("Unsupported user attribute: {}", attribute), _ => bail!("Unsupported user attribute: {}", attribute),
} }))
} }
fn make_ldap_search_user_result_entry( fn make_ldap_search_user_result_entry(
@ -124,47 +127,63 @@ fn make_ldap_search_user_result_entry(
dn: dn.clone(), dn: dn.clone(),
attributes: attributes attributes: attributes
.iter() .iter()
.map(|a| { .filter_map(|a| {
Ok(LdapPartialAttribute { let values = match get_user_attribute(&user, a, &dn) {
Err(e) => return Some(Err(e)),
Ok(v) => v,
}?;
Some(Ok(LdapPartialAttribute {
atype: a.to_string(), atype: a.to_string(),
vals: get_user_attribute(&user, a, &dn)?, vals: values,
}) }))
}) })
.collect::<Result<Vec<LdapPartialAttribute>>>()?, .collect::<Result<Vec<LdapPartialAttribute>>>()?,
}) })
} }
fn get_group_attribute(group: &Group, base_dn_str: &str, attribute: &str) -> Result<Vec<String>> { fn get_group_attribute(
match attribute.to_lowercase().as_str() { group: &Group,
"objectclass" => Ok(vec!["groupOfUniqueNames".to_string()]), base_dn_str: &str,
"dn" => Ok(vec![format!( attribute: &str,
user_filter: &Option<&UserId>,
) -> Result<Option<Vec<String>>> {
Ok(Some(match attribute.to_lowercase().as_str() {
"objectclass" => vec!["groupOfUniqueNames".to_string()],
"dn" => vec![format!(
"cn={},ou=groups,{}", "cn={},ou=groups,{}",
group.display_name, base_dn_str group.display_name, base_dn_str
)]), )],
"cn" | "uid" => Ok(vec![group.display_name.clone()]), "cn" | "uid" => vec![group.display_name.clone()],
"member" | "uniquemember" => Ok(group "member" | "uniquemember" => group
.users .users
.iter() .iter()
.filter(|u| user_filter.map(|f| *u == f).unwrap_or(true))
.map(|u| format!("cn={},ou=people,{}", u, base_dn_str)) .map(|u| format!("cn={},ou=people,{}", u, base_dn_str))
.collect()), .collect(),
"1.1" => return Ok(None),
_ => bail!("Unsupported group attribute: {}", attribute), _ => bail!("Unsupported group attribute: {}", attribute),
} }))
} }
fn make_ldap_search_group_result_entry( fn make_ldap_search_group_result_entry(
group: Group, group: Group,
base_dn_str: &str, base_dn_str: &str,
attributes: &[String], attributes: &[String],
user_filter: &Option<&UserId>,
) -> Result<LdapSearchResultEntry> { ) -> Result<LdapSearchResultEntry> {
Ok(LdapSearchResultEntry { Ok(LdapSearchResultEntry {
dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str), dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str),
attributes: attributes attributes: attributes
.iter() .iter()
.map(|a| { .filter_map(|a| {
Ok(LdapPartialAttribute { let values = match get_group_attribute(&group, base_dn_str, a, user_filter) {
Err(e) => return Some(Err(e)),
Ok(v) => v,
}?;
Some(Ok(LdapPartialAttribute {
atype: a.to_string(), atype: a.to_string(),
vals: get_group_attribute(&group, base_dn_str, a)?, vals: values,
}) }))
}) })
.collect::<Result<Vec<LdapPartialAttribute>>>()?, .collect::<Result<Vec<LdapPartialAttribute>>>()?,
}) })
@ -265,17 +284,19 @@ fn root_dse_response(base_dn: &str) -> LdapOp {
} }
pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> { pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> {
dn: UserId, dn: LdapDn,
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: UserId, 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, ldap_user_dn: UserId) -> Self {
Self { Self {
dn: UserId::new("unauthenticated"), dn: LdapDn("unauthenticated".to_string()),
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!(
@ -283,7 +304,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
ldap_base_dn ldap_base_dn
) )
}), }),
ldap_user_dn: UserId::new(&format!("cn={},ou=people,{}", ldap_user_dn, &ldap_base_dn)), ldap_user_dn: LdapDn(format!("cn={},ou=people,{}", ldap_user_dn, &ldap_base_dn)),
base_dn_str: ldap_base_dn, base_dn_str: ldap_base_dn,
} }
} }
@ -302,13 +323,14 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
match self match self
.backend_handler .backend_handler
.bind(BindRequest { .bind(BindRequest {
name: user_id, name: user_id.clone(),
password: password.clone(), password: password.clone(),
}) })
.await .await
{ {
Ok(()) => { Ok(()) => {
self.dn = UserId::new(&request.dn); self.dn = LdapDn(request.dn.clone());
self.user_id = user_id;
(LdapResultCode::Success, "".to_string()) (LdapResultCode::Success, "".to_string())
} }
Err(_) => (LdapResultCode::InvalidCredentials, "".to_string()), Err(_) => (LdapResultCode::InvalidCredentials, "".to_string()),
@ -382,15 +404,7 @@ 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> {
if self.dn != self.ldap_user_dn { let admin = self.dn == self.ldap_user_dn;
return vec![make_search_error(
LdapResultCode::InsufficentAccessRights,
format!(
r#"Current user `{}` is not allowed to query LDAP, expected {}"#,
&self.dn, &self.ldap_user_dn
),
)];
}
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())
@ -418,19 +432,20 @@ 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()))
{ {
got_match = true; got_match = true;
results.extend(self.get_user_list(request).await); results.extend(self.get_user_list(request, &user_filter).await);
} }
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(), "groups".to_string())) && dn_parts[0] == ("ou".to_string(), "groups".to_string()))
{ {
got_match = true; got_match = true;
results.extend(self.get_groups_list(request).await); results.extend(self.get_groups_list(request, &user_filter).await);
} }
if !got_match { if !got_match {
warn!( warn!(
@ -445,9 +460,13 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
results results
} }
async fn get_user_list(&self, request: &LdapSearchRequest) -> Vec<LdapOp> { async fn get_user_list(
&self,
request: &LdapSearchRequest,
user_filter: &Option<&UserId>,
) -> Vec<LdapOp> {
let filters = match self.convert_user_filter(&request.filter) { let filters = match self.convert_user_filter(&request.filter) {
Ok(f) => Some(f), Ok(f) => f,
Err(e) => { Err(e) => {
return vec![make_search_error( return vec![make_search_error(
LdapResultCode::UnwillingToPerform, LdapResultCode::UnwillingToPerform,
@ -455,7 +474,13 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
)] )]
} }
}; };
let users = match self.backend_handler.list_users(filters).await { let filters = match user_filter {
None => filters,
Some(u) => {
UserRequestFilter::And(vec![filters, UserRequestFilter::UserId((*u).clone())])
}
};
let users = match self.backend_handler.list_users(Some(filters)).await {
Ok(users) => users, Ok(users) => users,
Err(e) => { Err(e) => {
return vec![make_search_error( return vec![make_search_error(
@ -478,7 +503,11 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
}) })
} }
async fn get_groups_list(&self, request: &LdapSearchRequest) -> Vec<LdapOp> { async fn get_groups_list(
&self,
request: &LdapSearchRequest,
user_filter: &Option<&UserId>,
) -> Vec<LdapOp> {
let filter = match self.convert_group_filter(&request.filter) { let filter = match self.convert_group_filter(&request.filter) {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
@ -488,6 +517,12 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
)] )]
} }
}; };
let filter = match user_filter {
None => filter,
Some(u) => {
GroupRequestFilter::And(vec![filter, GroupRequestFilter::Member((*u).clone())])
}
};
let groups = match self.backend_handler.list_groups(Some(filter)).await { let groups = match self.backend_handler.list_groups(Some(filter)).await {
Ok(groups) => groups, Ok(groups) => groups,
@ -501,7 +536,14 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
groups groups
.into_iter() .into_iter()
.map(|u| make_ldap_search_group_result_entry(u, &self.base_dn_str, &request.attrs)) .map(|u| {
make_ldap_search_group_result_entry(
u,
&self.base_dn_str,
&request.attrs,
user_filter,
)
})
.map(|entry| Ok(LdapOp::SearchResultEntry(entry?))) .map(|entry| Ok(LdapOp::SearchResultEntry(entry?)))
.collect::<Result<Vec<_>>>() .collect::<Result<Vec<_>>>()
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
@ -528,7 +570,8 @@ 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 = UserId::new("unauthenticated"); self.dn = LdapDn("unauthenticated".to_string());
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;
} }
@ -801,7 +844,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn test_bind_invalid_credentials() { async fn test_search_non_admin_user() {
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();
mock.expect_bind() mock.expect_bind()
.with(eq(crate::domain::handler::BindRequest { .with(eq(crate::domain::handler::BindRequest {
@ -810,6 +853,18 @@ mod tests {
})) }))
.times(1) .times(1)
.return_once(|_| Ok(())); .return_once(|_| Ok(()));
mock.expect_list_users()
.with(eq(Some(UserRequestFilter::And(vec![
UserRequestFilter::And(vec![]),
UserRequestFilter::UserId(UserId::new("test")),
]))))
.times(1)
.return_once(|_| {
Ok(vec![User {
user_id: UserId::new("test"),
..Default::default()
}])
});
let mut ldap_handler = let mut ldap_handler =
LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("admin")); LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("admin"));
@ -822,13 +877,17 @@ mod tests {
LdapResultCode::Success LdapResultCode::Success
); );
let request = make_user_search_request::<String>(LdapFilter::And(vec![]), vec![]); let request =
make_user_search_request::<String>(LdapFilter::And(vec![]), vec!["1.1".to_string()]);
assert_eq!( assert_eq!(
ldap_handler.do_search(&request).await, ldap_handler.do_search(&request).await,
vec![make_search_error( vec![
LdapResultCode::InsufficentAccessRights, LdapOp::SearchResultEntry(LdapSearchResultEntry {
r#"Current user `cn=test,ou=people,dc=example,dc=com` is not allowed to query LDAP, expected cn=admin,ou=people,dc=example,dc=com"#.to_string() dn: "cn=test,ou=people,dc=example,dc=com".to_string(),
)] attributes: vec![],
}),
make_search_success()
],
); );
} }
@ -1144,17 +1203,14 @@ mod tests {
LdapFilter::Equality("objectclass".to_string(), "groupOfUniqueNames".to_string()), LdapFilter::Equality("objectclass".to_string(), "groupOfUniqueNames".to_string()),
LdapFilter::Equality("objectclass".to_string(), "groupOfNames".to_string()), LdapFilter::Equality("objectclass".to_string(), "groupOfNames".to_string()),
]), ]),
vec!["cn"], vec!["1.1"],
); );
assert_eq!( assert_eq!(
ldap_handler.do_search(&request).await, ldap_handler.do_search(&request).await,
vec![ vec![
LdapOp::SearchResultEntry(LdapSearchResultEntry { LdapOp::SearchResultEntry(LdapSearchResultEntry {
dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
attributes: vec![LdapPartialAttribute { attributes: vec![],
atype: "cn".to_string(),
vals: vec!["group_1".to_string()]
},],
}), }),
make_search_success(), make_search_success(),
] ]