ldap: add an option to silence unknown fields in the config

This commit is contained in:
Valentin Tolmer 2022-05-30 19:51:14 +02:00 committed by nitnelave
parent a0b0b455ed
commit 1efab58d0c
4 changed files with 127 additions and 29 deletions

View File

@ -78,6 +78,14 @@ database_url = "sqlite:///data/users.db?mode=rwc"
## Randomly generated on first run if it doesn't exist. ## Randomly generated on first run if it doesn't exist.
key_file = "/data/private_key" key_file = "/data/private_key"
## Ignored attributes.
## Some services will request attributes that are not present in LLDAP. When it
## is the case, LLDAP will warn about the attribute being unknown. If you want
## to ignore the attribute and the service works without, you can add it to this
## list to silence the warning.
#ignored_user_attributes = [ "sAMAccountName" ]
#ignored_group_attributes = [ "mail", "userPrincipalName" ]
## Options to configure SMTP parameters, to send password reset emails. ## Options to configure SMTP parameters, to send password reset emails.
## To set these options from environment variables, use the following format ## To set these options from environment variables, use the following format
## (example with "password"): LLDAP_SMTP_OPTIONS__PASSWORD ## (example with "password"): LLDAP_SMTP_OPTIONS__PASSWORD

View File

@ -75,6 +75,10 @@ pub struct Configuration {
pub ldap_user_pass: SecUtf8, pub ldap_user_pass: SecUtf8,
#[builder(default = r#"String::from("sqlite://users.db?mode=rwc")"#)] #[builder(default = r#"String::from("sqlite://users.db?mode=rwc")"#)]
pub database_url: String, pub database_url: String,
#[builder(default)]
pub ignored_user_attributes: Vec<String>,
#[builder(default)]
pub ignored_group_attributes: Vec<String>,
#[builder(default = "false")] #[builder(default = "false")]
pub verbose: bool, pub verbose: bool,
#[builder(default = r#"String::from("server_key")"#)] #[builder(default = r#"String::from("server_key")"#)]

View File

@ -102,8 +102,14 @@ fn get_user_id_from_distinguished_name(
} }
} }
fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result<Option<Vec<String>>> { fn get_user_attribute(
Ok(Some(match attribute.to_ascii_lowercase().as_str() { user: &User,
attribute: &str,
dn: &str,
ignored_user_attributes: &[String],
) -> Result<Option<Vec<String>>> {
let attribute = attribute.to_ascii_lowercase();
Ok(Some(match attribute.as_str() {
"objectclass" => vec![ "objectclass" => vec![
"inetOrgPerson".to_string(), "inetOrgPerson".to_string(),
"posixAccount".to_string(), "posixAccount".to_string(),
@ -118,7 +124,7 @@ fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result<Option<V
"cn" | "displayname" => vec![user.display_name.clone()], "cn" | "displayname" => vec![user.display_name.clone()],
"createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()], "createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()],
"1.1" => return Ok(None), "1.1" => return Ok(None),
// We ignore the operational attribute wildcard // We ignore the operational attribute wildcard.
"+" => return Ok(None), "+" => return Ok(None),
"*" => { "*" => {
bail!( bail!(
@ -127,7 +133,13 @@ fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result<Option<V
) )
} }
_ => { _ => {
warn!("Ignoring unrecognized group attribute: {}", attribute); if !ignored_user_attributes.contains(&attribute) {
warn!(
r#"Ignoring unrecognized group attribute: {}\n\
To disable this warning, add it to "ignored_user_attributes" in the config."#,
attribute
);
}
return Ok(None); return Ok(None);
} }
})) }))
@ -164,6 +176,7 @@ fn make_ldap_search_user_result_entry(
user: User, user: User,
base_dn_str: &str, base_dn_str: &str,
attributes: &[String], attributes: &[String],
ignored_user_attributes: &[String],
) -> Result<LdapSearchResultEntry> { ) -> Result<LdapSearchResultEntry> {
let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str);
@ -173,7 +186,7 @@ fn make_ldap_search_user_result_entry(
attributes: expanded_attributes attributes: expanded_attributes
.iter() .iter()
.filter_map(|a| { .filter_map(|a| {
let values = match get_user_attribute(&user, a, &dn) { let values = match get_user_attribute(&user, a, &dn, ignored_user_attributes) {
Err(e) => return Some(Err(e)), Err(e) => return Some(Err(e)),
Ok(v) => v, Ok(v) => v,
}?; }?;
@ -191,8 +204,10 @@ fn get_group_attribute(
base_dn_str: &str, base_dn_str: &str,
attribute: &str, attribute: &str,
user_filter: &Option<&UserId>, user_filter: &Option<&UserId>,
ignored_group_attributes: &[String],
) -> Result<Option<Vec<String>>> { ) -> Result<Option<Vec<String>>> {
Ok(Some(match attribute.to_ascii_lowercase().as_str() { let attribute = attribute.to_ascii_lowercase();
Ok(Some(match attribute.as_str() {
"objectclass" => vec!["groupOfUniqueNames".to_string()], "objectclass" => vec!["groupOfUniqueNames".to_string()],
"dn" | "distinguishedname" => vec![format!( "dn" | "distinguishedname" => vec![format!(
"cn={},ou=groups,{}", "cn={},ou=groups,{}",
@ -215,7 +230,13 @@ fn get_group_attribute(
) )
} }
_ => { _ => {
warn!("Ignoring unrecognized group attribute: {}", attribute); if !ignored_group_attributes.contains(&attribute) {
warn!(
r#"Ignoring unrecognized group attribute: {}\n\
To disable this warning, add it to "ignored_group_attributes" in the config."#,
attribute
);
}
return Ok(None); return Ok(None);
} }
})) }))
@ -229,6 +250,7 @@ fn make_ldap_search_group_result_entry(
base_dn_str: &str, base_dn_str: &str,
attributes: &[String], attributes: &[String],
user_filter: &Option<&UserId>, user_filter: &Option<&UserId>,
ignored_group_attributes: &[String],
) -> Result<LdapSearchResultEntry> { ) -> Result<LdapSearchResultEntry> {
let expanded_attributes = expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS); let expanded_attributes = expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS);
@ -237,7 +259,13 @@ fn make_ldap_search_group_result_entry(
attributes: expanded_attributes attributes: expanded_attributes
.iter() .iter()
.filter_map(|a| { .filter_map(|a| {
let values = match get_group_attribute(&group, base_dn_str, a, user_filter) { let values = match get_group_attribute(
&group,
base_dn_str,
a,
user_filter,
ignored_group_attributes,
) {
Err(e) => return Some(Err(e)), Err(e) => return Some(Err(e)),
Ok(v) => v, Ok(v) => v,
}?; }?;
@ -361,10 +389,17 @@ pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> {
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,
ignored_user_attributes: Vec<String>,
ignored_group_attributes: Vec<String>,
} }
impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> { impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> {
pub fn new(backend_handler: Backend, mut ldap_base_dn: String) -> Self { pub fn new(
backend_handler: Backend,
mut ldap_base_dn: String,
ignored_user_attributes: Vec<String>,
ignored_group_attributes: Vec<String>,
) -> Self {
ldap_base_dn.make_ascii_lowercase(); ldap_base_dn.make_ascii_lowercase();
Self { Self {
user_info: None, user_info: None,
@ -376,6 +411,8 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
) )
}), }),
base_dn_str: ldap_base_dn, base_dn_str: ldap_base_dn,
ignored_user_attributes,
ignored_group_attributes,
} }
} }
@ -600,7 +637,14 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
users users
.into_iter() .into_iter()
.map(|u| make_ldap_search_user_result_entry(u, &self.base_dn_str, &request.attrs)) .map(|u| {
make_ldap_search_user_result_entry(
u,
&self.base_dn_str,
&request.attrs,
&self.ignored_user_attributes,
)
})
.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| {
@ -650,6 +694,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
&self.base_dn_str, &self.base_dn_str,
&request.attrs, &request.attrs,
user_filter, user_filter,
&self.ignored_group_attributes,
) )
}) })
.map(|entry| Ok(LdapOp::SearchResultEntry(entry?))) .map(|entry| Ok(LdapOp::SearchResultEntry(entry?)))
@ -718,10 +763,13 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
{ {
Ok(GroupRequestFilter::DisplayName(value.clone())) Ok(GroupRequestFilter::DisplayName(value.clone()))
} else { } else {
if !self.ignored_group_attributes.contains(field) {
warn!( warn!(
r#"Ignoring unknown group attribute "{:?}" in filter"#, r#"Ignoring unknown group attribute "{:?}" in filter.\n\
To disable this warning, add it to "ignored_group_attributes" in the config."#,
field field
); );
}
Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And( Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(
vec![], vec![],
)))) ))))
@ -804,7 +852,13 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
} }
Err(_) => { Err(_) => {
warn!(r#"Ignoring unknown user attribute "{}" in filter"#, field); if !self.ignored_user_attributes.contains(field) {
warn!(
r#"Ignoring unknown user attribute "{}" in filter.\n\
To disable this warning, add it to "ignored_user_attributes" in the config"#,
field
);
}
Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And( Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And(
vec![], vec![],
)))) ))))
@ -921,7 +975,8 @@ mod tests {
set.insert(GroupIdAndName(GroupId(42), "lldap_admin".to_string())); set.insert(GroupIdAndName(GroupId(42), "lldap_admin".to_string()));
Ok(set) Ok(set)
}); });
let mut ldap_handler = LdapHandler::new(mock, "dc=Example,dc=com".to_string()); let mut ldap_handler =
LdapHandler::new(mock, "dc=Example,dc=com".to_string(), vec![], vec![]);
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()),
@ -946,7 +1001,8 @@ mod tests {
mock.expect_get_user_groups() mock.expect_get_user_groups()
.with(eq(UserId::new("bob"))) .with(eq(UserId::new("bob")))
.return_once(|_| Ok(HashSet::new())); .return_once(|_| Ok(HashSet::new()));
let mut ldap_handler = LdapHandler::new(mock, "dc=eXample,dc=com".to_string()); let mut ldap_handler =
LdapHandler::new(mock, "dc=eXample,dc=com".to_string(), vec![], vec![]);
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(),
@ -983,7 +1039,8 @@ mod tests {
set.insert(GroupIdAndName(GroupId(42), "lldap_admin".to_string())); set.insert(GroupIdAndName(GroupId(42), "lldap_admin".to_string()));
Ok(set) Ok(set)
}); });
let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string()); let mut ldap_handler =
LdapHandler::new(mock, "dc=example,dc=com".to_string(), vec![], vec![]);
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(),
@ -1020,7 +1077,8 @@ mod tests {
mock.expect_get_user_groups() mock.expect_get_user_groups()
.with(eq(UserId::new("test"))) .with(eq(UserId::new("test")))
.return_once(|_| Ok(HashSet::new())); .return_once(|_| Ok(HashSet::new()));
let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string()); let mut ldap_handler =
LdapHandler::new(mock, "dc=example,dc=com".to_string(), vec![], vec![]);
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(),
@ -1048,7 +1106,8 @@ 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 = LdapHandler::new(mock, "dc=example,dc=com".to_string()); let mut ldap_handler =
LdapHandler::new(mock, "dc=example,dc=com".to_string(), vec![], vec![]);
let request = LdapBindRequest { let request = LdapBindRequest {
dn: "cn=bob,dc=example,dc=com".to_string(), dn: "cn=bob,dc=example,dc=com".to_string(),
@ -1348,9 +1407,7 @@ mod tests {
GroupRequestFilter::Not(Box::new(GroupRequestFilter::Not(Box::new( GroupRequestFilter::Not(Box::new(GroupRequestFilter::Not(Box::new(
GroupRequestFilter::And(vec![]), GroupRequestFilter::And(vec![]),
)))), )))),
GroupRequestFilter::Not(Box::new( GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(vec![]))),
GroupRequestFilter::And(vec![]),
)),
])))) ]))))
.times(1) .times(1)
.return_once(|_| { .return_once(|_| {

View File

@ -70,6 +70,8 @@ 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,
ignored_user_attributes: Vec<String>,
ignored_group_attributes: Vec<String>,
) -> Result<Stream> ) -> Result<Stream>
where where
Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static, Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static,
@ -81,7 +83,12 @@ 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); let mut session = LdapHandler::new(
backend_handler,
ldap_base_dn,
ignored_user_attributes,
ignored_group_attributes,
);
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)
@ -110,7 +117,12 @@ pub fn build_ldap_server<Backend>(
where where
Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static, Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static,
{ {
let context = (backend_handler, config.ldap_base_dn.clone()); let context = (
backend_handler,
config.ldap_base_dn.clone(),
config.ignored_user_attributes.clone(),
config.ignored_group_attributes.clone(),
);
let context_for_tls = context.clone(); let context_for_tls = context.clone();
@ -119,8 +131,15 @@ 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) = context; let (handler, base_dn, ignored_user_attributes, ignored_group_attributes) = context;
handle_ldap_stream(stream, handler, base_dn).await handle_ldap_stream(
stream,
handler,
base_dn,
ignored_user_attributes,
ignored_group_attributes,
)
.await
} }
}) })
.map_err(|err: anyhow::Error| error!("[LDAP] Service Error: {:#}", err)) .map_err(|err: anyhow::Error| error!("[LDAP] Service Error: {:#}", err))
@ -139,9 +158,19 @@ 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), tls_acceptor) = tls_context; let (
(handler, base_dn, ignored_user_attributes, ignored_group_attributes),
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).await handle_ldap_stream(
tls_stream,
handler,
base_dn,
ignored_user_attributes,
ignored_group_attributes,
)
.await
} }
}) })
.map_err(|err: anyhow::Error| error!("[LDAPS] Service Error: {:#}", err)) .map_err(|err: anyhow::Error| error!("[LDAPS] Service Error: {:#}", err))