mirror of
				https://github.com/nitnelave/lldap.git
				synced 2023-04-12 14:25:13 +00:00 
			
		
		
		
	server: refactor ldap_handler
Split it into several files, move them into the domain folder, introduce `LdapError` for better control flow.
This commit is contained in:
		
							parent
							
								
									0be440efc8
								
							
						
					
					
						commit
						85b4fc4406
					
				
							
								
								
									
										17
									
								
								server/src/domain/ldap/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								server/src/domain/ldap/error.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					use ldap3_proto::LdapResultCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, PartialEq)]
 | 
				
			||||||
 | 
					pub struct LdapError {
 | 
				
			||||||
 | 
					    pub code: LdapResultCode,
 | 
				
			||||||
 | 
					    pub message: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl std::fmt::Display for LdapError {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        f.write_str(&self.message)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl std::error::Error for LdapError {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub type LdapResult<T> = std::result::Result<T, LdapError>;
 | 
				
			||||||
							
								
								
									
										218
									
								
								server/src/domain/ldap/group.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								server/src/domain/ldap/group.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,218 @@
 | 
				
			|||||||
 | 
					use ldap3_proto::{
 | 
				
			||||||
 | 
					    proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use tracing::{debug, info, instrument, warn};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::domain::{
 | 
				
			||||||
 | 
					    handler::{BackendHandler, Group, GroupRequestFilter, UserId, Uuid},
 | 
				
			||||||
 | 
					    ldap::error::LdapError,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{
 | 
				
			||||||
 | 
					    error::LdapResult,
 | 
				
			||||||
 | 
					    utils::{expand_attribute_wildcards, get_user_id_from_distinguished_name, map_field, LdapInfo},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn get_group_attribute(
 | 
				
			||||||
 | 
					    group: &Group,
 | 
				
			||||||
 | 
					    base_dn_str: &str,
 | 
				
			||||||
 | 
					    attribute: &str,
 | 
				
			||||||
 | 
					    user_filter: &Option<&UserId>,
 | 
				
			||||||
 | 
					    ignored_group_attributes: &[String],
 | 
				
			||||||
 | 
					) -> Option<Vec<Vec<u8>>> {
 | 
				
			||||||
 | 
					    let attribute = attribute.to_ascii_lowercase();
 | 
				
			||||||
 | 
					    let attribute_values = match attribute.as_str() {
 | 
				
			||||||
 | 
					        "objectclass" => vec![b"groupOfUniqueNames".to_vec()],
 | 
				
			||||||
 | 
					        // Always returned as part of the base response.
 | 
				
			||||||
 | 
					        "dn" | "distinguishedname" => return None,
 | 
				
			||||||
 | 
					        "cn" | "uid" => vec![group.display_name.clone().into_bytes()],
 | 
				
			||||||
 | 
					        "entryuuid" => vec![group.uuid.to_string().into_bytes()],
 | 
				
			||||||
 | 
					        "member" | "uniquemember" => group
 | 
				
			||||||
 | 
					            .users
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .filter(|u| user_filter.map(|f| *u == f).unwrap_or(true))
 | 
				
			||||||
 | 
					            .map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes())
 | 
				
			||||||
 | 
					            .collect(),
 | 
				
			||||||
 | 
					        "1.1" => return None,
 | 
				
			||||||
 | 
					        // We ignore the operational attribute wildcard
 | 
				
			||||||
 | 
					        "+" => return None,
 | 
				
			||||||
 | 
					        "*" => {
 | 
				
			||||||
 | 
					            panic!(
 | 
				
			||||||
 | 
					                "Matched {}, * should have been expanded into attribute list and * removed",
 | 
				
			||||||
 | 
					                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 None;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    if attribute_values.len() == 1 && attribute_values[0].is_empty() {
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Some(attribute_values)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ALL_GROUP_ATTRIBUTE_KEYS: &[&str] = &[
 | 
				
			||||||
 | 
					    "objectclass",
 | 
				
			||||||
 | 
					    "uid",
 | 
				
			||||||
 | 
					    "cn",
 | 
				
			||||||
 | 
					    "member",
 | 
				
			||||||
 | 
					    "uniquemember",
 | 
				
			||||||
 | 
					    "entryuuid",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn make_ldap_search_group_result_entry(
 | 
				
			||||||
 | 
					    group: Group,
 | 
				
			||||||
 | 
					    base_dn_str: &str,
 | 
				
			||||||
 | 
					    attributes: &[String],
 | 
				
			||||||
 | 
					    user_filter: &Option<&UserId>,
 | 
				
			||||||
 | 
					    ignored_group_attributes: &[String],
 | 
				
			||||||
 | 
					) -> LdapSearchResultEntry {
 | 
				
			||||||
 | 
					    let expanded_attributes = expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    LdapSearchResultEntry {
 | 
				
			||||||
 | 
					        dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str),
 | 
				
			||||||
 | 
					        attributes: expanded_attributes
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .filter_map(|a| {
 | 
				
			||||||
 | 
					                let values = get_group_attribute(
 | 
				
			||||||
 | 
					                    &group,
 | 
				
			||||||
 | 
					                    base_dn_str,
 | 
				
			||||||
 | 
					                    a,
 | 
				
			||||||
 | 
					                    user_filter,
 | 
				
			||||||
 | 
					                    ignored_group_attributes,
 | 
				
			||||||
 | 
					                )?;
 | 
				
			||||||
 | 
					                Some(LdapPartialAttribute {
 | 
				
			||||||
 | 
					                    atype: a.to_string(),
 | 
				
			||||||
 | 
					                    vals: values,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .collect::<Vec<LdapPartialAttribute>>(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn convert_group_filter(
 | 
				
			||||||
 | 
					    ldap_info: &LdapInfo,
 | 
				
			||||||
 | 
					    filter: &LdapFilter,
 | 
				
			||||||
 | 
					) -> LdapResult<GroupRequestFilter> {
 | 
				
			||||||
 | 
					    let rec = |f| convert_group_filter(ldap_info, f);
 | 
				
			||||||
 | 
					    match filter {
 | 
				
			||||||
 | 
					        LdapFilter::Equality(field, value) => {
 | 
				
			||||||
 | 
					            let field = &field.to_ascii_lowercase();
 | 
				
			||||||
 | 
					            let value = &value.to_ascii_lowercase();
 | 
				
			||||||
 | 
					            match field.as_str() {
 | 
				
			||||||
 | 
					                "member" | "uniquemember" => {
 | 
				
			||||||
 | 
					                    let user_name = get_user_id_from_distinguished_name(
 | 
				
			||||||
 | 
					                        value,
 | 
				
			||||||
 | 
					                        &ldap_info.base_dn,
 | 
				
			||||||
 | 
					                        &ldap_info.base_dn_str,
 | 
				
			||||||
 | 
					                    )?;
 | 
				
			||||||
 | 
					                    Ok(GroupRequestFilter::Member(user_name))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                "objectclass" => match value.as_str() {
 | 
				
			||||||
 | 
					                    "groupofuniquenames" | "groupofnames" => Ok(GroupRequestFilter::And(vec![])),
 | 
				
			||||||
 | 
					                    _ => Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(
 | 
				
			||||||
 | 
					                        vec![],
 | 
				
			||||||
 | 
					                    )))),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                _ => match map_field(field) {
 | 
				
			||||||
 | 
					                    Some("display_name") | Some("user_id") => {
 | 
				
			||||||
 | 
					                        Ok(GroupRequestFilter::DisplayName(value.to_string()))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    Some("uuid") => Ok(GroupRequestFilter::Uuid(
 | 
				
			||||||
 | 
					                        Uuid::try_from(value.as_str()).map_err(|e| LdapError {
 | 
				
			||||||
 | 
					                            code: LdapResultCode::InappropriateMatching,
 | 
				
			||||||
 | 
					                            message: format!("Invalid UUID: {:#}", e),
 | 
				
			||||||
 | 
					                        })?,
 | 
				
			||||||
 | 
					                    )),
 | 
				
			||||||
 | 
					                    _ => {
 | 
				
			||||||
 | 
					                        if !ldap_info.ignored_group_attributes.contains(field) {
 | 
				
			||||||
 | 
					                            warn!(
 | 
				
			||||||
 | 
					                                r#"Ignoring unknown group attribute "{:?}" in filter.\n\
 | 
				
			||||||
 | 
					                                To disable this warning, add it to "ignored_group_attributes" in the config."#,
 | 
				
			||||||
 | 
					                                field
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(
 | 
				
			||||||
 | 
					                            vec![],
 | 
				
			||||||
 | 
					                        ))))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        LdapFilter::And(filters) => Ok(GroupRequestFilter::And(
 | 
				
			||||||
 | 
					            filters.iter().map(rec).collect::<LdapResult<_>>()?,
 | 
				
			||||||
 | 
					        )),
 | 
				
			||||||
 | 
					        LdapFilter::Or(filters) => Ok(GroupRequestFilter::Or(
 | 
				
			||||||
 | 
					            filters.iter().map(rec).collect::<LdapResult<_>>()?,
 | 
				
			||||||
 | 
					        )),
 | 
				
			||||||
 | 
					        LdapFilter::Not(filter) => Ok(GroupRequestFilter::Not(Box::new(rec(filter)?))),
 | 
				
			||||||
 | 
					        LdapFilter::Present(field) => {
 | 
				
			||||||
 | 
					            let field = &field.to_ascii_lowercase();
 | 
				
			||||||
 | 
					            if field == "objectclass"
 | 
				
			||||||
 | 
					                || field == "dn"
 | 
				
			||||||
 | 
					                || field == "distinguishedname"
 | 
				
			||||||
 | 
					                || ALL_GROUP_ATTRIBUTE_KEYS.contains(&field.as_str())
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Ok(GroupRequestFilter::And(vec![]))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(
 | 
				
			||||||
 | 
					                    vec![],
 | 
				
			||||||
 | 
					                ))))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        _ => Err(LdapError {
 | 
				
			||||||
 | 
					            code: LdapResultCode::UnwillingToPerform,
 | 
				
			||||||
 | 
					            message: format!("Unsupported group filter: {:?}", filter),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[instrument(skip_all, level = "debug")]
 | 
				
			||||||
 | 
					pub async fn get_groups_list<Backend: BackendHandler>(
 | 
				
			||||||
 | 
					    ldap_info: &LdapInfo,
 | 
				
			||||||
 | 
					    ldap_filter: &LdapFilter,
 | 
				
			||||||
 | 
					    attributes: &[String],
 | 
				
			||||||
 | 
					    base: &str,
 | 
				
			||||||
 | 
					    user_filter: &Option<&UserId>,
 | 
				
			||||||
 | 
					    backend: &mut Backend,
 | 
				
			||||||
 | 
					) -> LdapResult<Vec<LdapOp>> {
 | 
				
			||||||
 | 
					    debug!(?ldap_filter);
 | 
				
			||||||
 | 
					    let filter = convert_group_filter(ldap_info, ldap_filter)?;
 | 
				
			||||||
 | 
					    let parsed_filters = match user_filter {
 | 
				
			||||||
 | 
					        None => filter,
 | 
				
			||||||
 | 
					        Some(u) => {
 | 
				
			||||||
 | 
					            info!("Unprivileged search, limiting results");
 | 
				
			||||||
 | 
					            GroupRequestFilter::And(vec![filter, GroupRequestFilter::Member((*u).clone())])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    debug!(?parsed_filters);
 | 
				
			||||||
 | 
					    let groups = backend
 | 
				
			||||||
 | 
					        .list_groups(Some(parsed_filters))
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map_err(|e| LdapError {
 | 
				
			||||||
 | 
					            code: LdapResultCode::Other,
 | 
				
			||||||
 | 
					            message: format!(r#"Error while listing groups "{}": {:#}"#, base, e),
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(groups
 | 
				
			||||||
 | 
					        .into_iter()
 | 
				
			||||||
 | 
					        .map(|u| {
 | 
				
			||||||
 | 
					            LdapOp::SearchResultEntry(make_ldap_search_group_result_entry(
 | 
				
			||||||
 | 
					                u,
 | 
				
			||||||
 | 
					                &ldap_info.base_dn_str,
 | 
				
			||||||
 | 
					                attributes,
 | 
				
			||||||
 | 
					                user_filter,
 | 
				
			||||||
 | 
					                &ldap_info.ignored_group_attributes,
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .collect::<Vec<_>>())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								server/src/domain/ldap/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								server/src/domain/ldap/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					pub mod error;
 | 
				
			||||||
 | 
					pub mod group;
 | 
				
			||||||
 | 
					pub mod user;
 | 
				
			||||||
 | 
					pub mod utils;
 | 
				
			||||||
							
								
								
									
										236
									
								
								server/src/domain/ldap/user.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								server/src/domain/ldap/user.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,236 @@
 | 
				
			|||||||
 | 
					use ldap3_proto::{
 | 
				
			||||||
 | 
					    proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use tracing::{debug, info, instrument, warn};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::domain::{
 | 
				
			||||||
 | 
					    handler::{BackendHandler, GroupDetails, User, UserId, UserRequestFilter},
 | 
				
			||||||
 | 
					    ldap::{error::LdapError, utils::expand_attribute_wildcards},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{
 | 
				
			||||||
 | 
					    error::LdapResult,
 | 
				
			||||||
 | 
					    utils::{get_group_id_from_distinguished_name, map_field, LdapInfo},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn get_user_attribute(
 | 
				
			||||||
 | 
					    user: &User,
 | 
				
			||||||
 | 
					    attribute: &str,
 | 
				
			||||||
 | 
					    base_dn_str: &str,
 | 
				
			||||||
 | 
					    groups: Option<&[GroupDetails]>,
 | 
				
			||||||
 | 
					    ignored_user_attributes: &[String],
 | 
				
			||||||
 | 
					) -> Option<Vec<Vec<u8>>> {
 | 
				
			||||||
 | 
					    let attribute = attribute.to_ascii_lowercase();
 | 
				
			||||||
 | 
					    let attribute_values = match attribute.as_str() {
 | 
				
			||||||
 | 
					        "objectclass" => vec![
 | 
				
			||||||
 | 
					            b"inetOrgPerson".to_vec(),
 | 
				
			||||||
 | 
					            b"posixAccount".to_vec(),
 | 
				
			||||||
 | 
					            b"mailAccount".to_vec(),
 | 
				
			||||||
 | 
					            b"person".to_vec(),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        // dn is always returned as part of the base response.
 | 
				
			||||||
 | 
					        "dn" | "distinguishedname" => return None,
 | 
				
			||||||
 | 
					        "uid" => vec![user.user_id.to_string().into_bytes()],
 | 
				
			||||||
 | 
					        "entryuuid" => vec![user.uuid.to_string().into_bytes()],
 | 
				
			||||||
 | 
					        "mail" => vec![user.email.clone().into_bytes()],
 | 
				
			||||||
 | 
					        "givenname" => vec![user.first_name.clone().into_bytes()],
 | 
				
			||||||
 | 
					        "sn" => vec![user.last_name.clone().into_bytes()],
 | 
				
			||||||
 | 
					        "jpegphoto" => vec![user.avatar.clone().into_bytes()],
 | 
				
			||||||
 | 
					        "memberof" => groups
 | 
				
			||||||
 | 
					            .into_iter()
 | 
				
			||||||
 | 
					            .flatten()
 | 
				
			||||||
 | 
					            .map(|id_and_name| {
 | 
				
			||||||
 | 
					                format!(
 | 
				
			||||||
 | 
					                    "uid={},ou=groups,{}",
 | 
				
			||||||
 | 
					                    &id_and_name.display_name, base_dn_str
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .into_bytes()
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .collect(),
 | 
				
			||||||
 | 
					        "cn" | "displayname" => vec![user.display_name.clone().into_bytes()],
 | 
				
			||||||
 | 
					        "createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339().into_bytes()],
 | 
				
			||||||
 | 
					        "1.1" => return None,
 | 
				
			||||||
 | 
					        // We ignore the operational attribute wildcard.
 | 
				
			||||||
 | 
					        "+" => return None,
 | 
				
			||||||
 | 
					        "*" => {
 | 
				
			||||||
 | 
					            panic!(
 | 
				
			||||||
 | 
					                "Matched {}, * should have been expanded into attribute list and * removed",
 | 
				
			||||||
 | 
					                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 None;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    if attribute_values.len() == 1 && attribute_values[0].is_empty() {
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Some(attribute_values)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ALL_USER_ATTRIBUTE_KEYS: &[&str] = &[
 | 
				
			||||||
 | 
					    "objectclass",
 | 
				
			||||||
 | 
					    "uid",
 | 
				
			||||||
 | 
					    "mail",
 | 
				
			||||||
 | 
					    "givenname",
 | 
				
			||||||
 | 
					    "sn",
 | 
				
			||||||
 | 
					    "cn",
 | 
				
			||||||
 | 
					    "jpegPhoto",
 | 
				
			||||||
 | 
					    "createtimestamp",
 | 
				
			||||||
 | 
					    "entryuuid",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn make_ldap_search_user_result_entry(
 | 
				
			||||||
 | 
					    user: User,
 | 
				
			||||||
 | 
					    base_dn_str: &str,
 | 
				
			||||||
 | 
					    attributes: &[&str],
 | 
				
			||||||
 | 
					    groups: Option<&[GroupDetails]>,
 | 
				
			||||||
 | 
					    ignored_user_attributes: &[String],
 | 
				
			||||||
 | 
					) -> LdapSearchResultEntry {
 | 
				
			||||||
 | 
					    let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    LdapSearchResultEntry {
 | 
				
			||||||
 | 
					        dn,
 | 
				
			||||||
 | 
					        attributes: attributes
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .filter_map(|a| {
 | 
				
			||||||
 | 
					                let values =
 | 
				
			||||||
 | 
					                    get_user_attribute(&user, a, base_dn_str, groups, ignored_user_attributes)?;
 | 
				
			||||||
 | 
					                Some(LdapPartialAttribute {
 | 
				
			||||||
 | 
					                    atype: a.to_string(),
 | 
				
			||||||
 | 
					                    vals: values,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .collect::<Vec<LdapPartialAttribute>>(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult<UserRequestFilter> {
 | 
				
			||||||
 | 
					    let rec = |f| convert_user_filter(ldap_info, f);
 | 
				
			||||||
 | 
					    match filter {
 | 
				
			||||||
 | 
					        LdapFilter::And(filters) => Ok(UserRequestFilter::And(
 | 
				
			||||||
 | 
					            filters.iter().map(rec).collect::<LdapResult<_>>()?,
 | 
				
			||||||
 | 
					        )),
 | 
				
			||||||
 | 
					        LdapFilter::Or(filters) => Ok(UserRequestFilter::Or(
 | 
				
			||||||
 | 
					            filters.iter().map(rec).collect::<LdapResult<_>>()?,
 | 
				
			||||||
 | 
					        )),
 | 
				
			||||||
 | 
					        LdapFilter::Not(filter) => Ok(UserRequestFilter::Not(Box::new(rec(filter)?))),
 | 
				
			||||||
 | 
					        LdapFilter::Equality(field, value) => {
 | 
				
			||||||
 | 
					            let field = &field.to_ascii_lowercase();
 | 
				
			||||||
 | 
					            match field.as_str() {
 | 
				
			||||||
 | 
					                "memberof" => {
 | 
				
			||||||
 | 
					                    let group_name = get_group_id_from_distinguished_name(
 | 
				
			||||||
 | 
					                        &value.to_ascii_lowercase(),
 | 
				
			||||||
 | 
					                        &ldap_info.base_dn,
 | 
				
			||||||
 | 
					                        &ldap_info.base_dn_str,
 | 
				
			||||||
 | 
					                    )?;
 | 
				
			||||||
 | 
					                    Ok(UserRequestFilter::MemberOf(group_name))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                "objectclass" => match value.to_ascii_lowercase().as_str() {
 | 
				
			||||||
 | 
					                    "person" | "inetorgperson" | "posixaccount" | "mailaccount" => {
 | 
				
			||||||
 | 
					                        Ok(UserRequestFilter::And(vec![]))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    _ => Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And(
 | 
				
			||||||
 | 
					                        vec![],
 | 
				
			||||||
 | 
					                    )))),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                _ => match map_field(field) {
 | 
				
			||||||
 | 
					                    Some(field) => {
 | 
				
			||||||
 | 
					                        if field == "user_id" {
 | 
				
			||||||
 | 
					                            Ok(UserRequestFilter::UserId(UserId::new(value)))
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            Ok(UserRequestFilter::Equality(
 | 
				
			||||||
 | 
					                                field.to_string(),
 | 
				
			||||||
 | 
					                                value.clone(),
 | 
				
			||||||
 | 
					                            ))
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    None => {
 | 
				
			||||||
 | 
					                        if !ldap_info.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(
 | 
				
			||||||
 | 
					                            vec![],
 | 
				
			||||||
 | 
					                        ))))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        LdapFilter::Present(field) => {
 | 
				
			||||||
 | 
					            let field = &field.to_ascii_lowercase();
 | 
				
			||||||
 | 
					            // Check that it's a field we support.
 | 
				
			||||||
 | 
					            if field == "objectclass"
 | 
				
			||||||
 | 
					                || field == "dn"
 | 
				
			||||||
 | 
					                || field == "distinguishedname"
 | 
				
			||||||
 | 
					                || ALL_USER_ATTRIBUTE_KEYS.contains(&field.as_str())
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Ok(UserRequestFilter::And(vec![]))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And(
 | 
				
			||||||
 | 
					                    vec![],
 | 
				
			||||||
 | 
					                ))))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        _ => Err(LdapError {
 | 
				
			||||||
 | 
					            code: LdapResultCode::UnwillingToPerform,
 | 
				
			||||||
 | 
					            message: format!("Unsupported user filter: {:?}", filter),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[instrument(skip_all, level = "debug")]
 | 
				
			||||||
 | 
					pub async fn get_user_list<Backend: BackendHandler>(
 | 
				
			||||||
 | 
					    ldap_info: &LdapInfo,
 | 
				
			||||||
 | 
					    ldap_filter: &LdapFilter,
 | 
				
			||||||
 | 
					    attributes: &[String],
 | 
				
			||||||
 | 
					    base: &str,
 | 
				
			||||||
 | 
					    user_filter: &Option<&UserId>,
 | 
				
			||||||
 | 
					    backend: &mut Backend,
 | 
				
			||||||
 | 
					) -> LdapResult<Vec<LdapOp>> {
 | 
				
			||||||
 | 
					    debug!(?ldap_filter);
 | 
				
			||||||
 | 
					    let filters = convert_user_filter(ldap_info, ldap_filter)?;
 | 
				
			||||||
 | 
					    let parsed_filters = match user_filter {
 | 
				
			||||||
 | 
					        None => filters,
 | 
				
			||||||
 | 
					        Some(u) => {
 | 
				
			||||||
 | 
					            info!("Unprivileged search, limiting results");
 | 
				
			||||||
 | 
					            UserRequestFilter::And(vec![filters, UserRequestFilter::UserId((*u).clone())])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    debug!(?parsed_filters);
 | 
				
			||||||
 | 
					    let expanded_attributes = expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS);
 | 
				
			||||||
 | 
					    let need_groups = expanded_attributes
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .any(|s| s.to_ascii_lowercase() == "memberof");
 | 
				
			||||||
 | 
					    let users = backend
 | 
				
			||||||
 | 
					        .list_users(Some(parsed_filters), need_groups)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map_err(|e| LdapError {
 | 
				
			||||||
 | 
					            code: LdapResultCode::Other,
 | 
				
			||||||
 | 
					            message: format!(r#"Error while searching user "{}": {:#}"#, base, e),
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(users
 | 
				
			||||||
 | 
					        .into_iter()
 | 
				
			||||||
 | 
					        .map(|u| {
 | 
				
			||||||
 | 
					            LdapOp::SearchResultEntry(make_ldap_search_user_result_entry(
 | 
				
			||||||
 | 
					                u.user,
 | 
				
			||||||
 | 
					                &ldap_info.base_dn_str,
 | 
				
			||||||
 | 
					                &expanded_attributes,
 | 
				
			||||||
 | 
					                u.groups.as_deref(),
 | 
				
			||||||
 | 
					                &ldap_info.ignored_user_attributes,
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .collect::<Vec<_>>())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										157
									
								
								server/src/domain/ldap/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								server/src/domain/ldap/utils.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,157 @@
 | 
				
			|||||||
 | 
					use itertools::Itertools;
 | 
				
			||||||
 | 
					use ldap3_proto::LdapResultCode;
 | 
				
			||||||
 | 
					use tracing::{debug, instrument, warn};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::domain::handler::UserId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::error::{LdapError, LdapResult};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn make_dn_pair<I>(mut iter: I) -> LdapResult<(String, String)>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    I: Iterator<Item = String>,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    (|| {
 | 
				
			||||||
 | 
					        let pair = (
 | 
				
			||||||
 | 
					            iter.next().ok_or_else(|| "Empty DN element".to_string())?,
 | 
				
			||||||
 | 
					            iter.next().ok_or_else(|| "Missing DN value".to_string())?,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        if let Some(e) = iter.next() {
 | 
				
			||||||
 | 
					            Err(format!(
 | 
				
			||||||
 | 
					                r#"Too many elements in distinguished name: "{:?}", "{:?}", "{:?}""#,
 | 
				
			||||||
 | 
					                pair.0, pair.1, e
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Ok(pair)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })()
 | 
				
			||||||
 | 
					    .map_err(|s| LdapError {
 | 
				
			||||||
 | 
					        code: LdapResultCode::InvalidDNSyntax,
 | 
				
			||||||
 | 
					        message: s,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn parse_distinguished_name(dn: &str) -> LdapResult<Vec<(String, String)>> {
 | 
				
			||||||
 | 
					    assert!(dn == dn.to_ascii_lowercase());
 | 
				
			||||||
 | 
					    dn.split(',')
 | 
				
			||||||
 | 
					        .map(|s| make_dn_pair(s.split('=').map(str::trim).map(String::from)))
 | 
				
			||||||
 | 
					        .collect()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn get_id_from_distinguished_name(
 | 
				
			||||||
 | 
					    dn: &str,
 | 
				
			||||||
 | 
					    base_tree: &[(String, String)],
 | 
				
			||||||
 | 
					    base_dn_str: &str,
 | 
				
			||||||
 | 
					    is_group: bool,
 | 
				
			||||||
 | 
					) -> LdapResult<String> {
 | 
				
			||||||
 | 
					    let parts = parse_distinguished_name(dn)?;
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let ou = if is_group { "groups" } else { "people" };
 | 
				
			||||||
 | 
					        if !is_subtree(&parts, base_tree) {
 | 
				
			||||||
 | 
					            Err("Not a subtree of the base tree".to_string())
 | 
				
			||||||
 | 
					        } else if parts.len() == base_tree.len() + 2 {
 | 
				
			||||||
 | 
					            if parts[1].0 != "ou" || parts[1].1 != ou || (parts[0].0 != "cn" && parts[0].0 != "uid")
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Err(format!(
 | 
				
			||||||
 | 
					                    r#"Unexpected DN format. Got "{}", expected: "uid=id,ou={},{}""#,
 | 
				
			||||||
 | 
					                    dn, ou, base_dn_str
 | 
				
			||||||
 | 
					                ))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Ok(parts[0].1.to_string())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(format!(
 | 
				
			||||||
 | 
					                r#"Unexpected DN format. Got "{}", expected: "uid=id,ou={},{}""#,
 | 
				
			||||||
 | 
					                dn, ou, base_dn_str
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .map_err(|s| LdapError {
 | 
				
			||||||
 | 
					        code: LdapResultCode::InvalidDNSyntax,
 | 
				
			||||||
 | 
					        message: s,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn get_user_id_from_distinguished_name(
 | 
				
			||||||
 | 
					    dn: &str,
 | 
				
			||||||
 | 
					    base_tree: &[(String, String)],
 | 
				
			||||||
 | 
					    base_dn_str: &str,
 | 
				
			||||||
 | 
					) -> LdapResult<UserId> {
 | 
				
			||||||
 | 
					    get_id_from_distinguished_name(dn, base_tree, base_dn_str, false).map(UserId::from)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn get_group_id_from_distinguished_name(
 | 
				
			||||||
 | 
					    dn: &str,
 | 
				
			||||||
 | 
					    base_tree: &[(String, String)],
 | 
				
			||||||
 | 
					    base_dn_str: &str,
 | 
				
			||||||
 | 
					) -> LdapResult<String> {
 | 
				
			||||||
 | 
					    get_id_from_distinguished_name(dn, base_tree, base_dn_str, true)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[instrument(skip_all, level = "debug")]
 | 
				
			||||||
 | 
					pub fn expand_attribute_wildcards<'a>(
 | 
				
			||||||
 | 
					    ldap_attributes: &'a [String],
 | 
				
			||||||
 | 
					    all_attribute_keys: &'a [&'static str],
 | 
				
			||||||
 | 
					) -> Vec<&'a str> {
 | 
				
			||||||
 | 
					    let mut attributes_out = ldap_attributes
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .map(String::as_str)
 | 
				
			||||||
 | 
					        .collect::<Vec<_>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if attributes_out.iter().any(|&x| x == "*") || attributes_out.is_empty() {
 | 
				
			||||||
 | 
					        // Remove occurrences of '*'
 | 
				
			||||||
 | 
					        attributes_out.retain(|&x| x != "*");
 | 
				
			||||||
 | 
					        // Splice in all non-operational attributes
 | 
				
			||||||
 | 
					        attributes_out.extend(all_attribute_keys.iter());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Deduplicate, preserving order
 | 
				
			||||||
 | 
					    let resolved_attributes = attributes_out
 | 
				
			||||||
 | 
					        .into_iter()
 | 
				
			||||||
 | 
					        .unique_by(|a| a.to_ascii_lowercase())
 | 
				
			||||||
 | 
					        .collect_vec();
 | 
				
			||||||
 | 
					    debug!(?ldap_attributes, ?resolved_attributes);
 | 
				
			||||||
 | 
					    resolved_attributes
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)]) -> bool {
 | 
				
			||||||
 | 
					    for (k, v) in subtree {
 | 
				
			||||||
 | 
					        assert!(k == &k.to_ascii_lowercase());
 | 
				
			||||||
 | 
					        assert!(v == &v.to_ascii_lowercase());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (k, v) in base_tree {
 | 
				
			||||||
 | 
					        assert!(k == &k.to_ascii_lowercase());
 | 
				
			||||||
 | 
					        assert!(v == &v.to_ascii_lowercase());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if subtree.len() < base_tree.len() {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let size_diff = subtree.len() - base_tree.len();
 | 
				
			||||||
 | 
					    for i in 0..base_tree.len() {
 | 
				
			||||||
 | 
					        if subtree[size_diff + i] != base_tree[i] {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn map_field(field: &str) -> Option<&'static str> {
 | 
				
			||||||
 | 
					    assert!(field == field.to_ascii_lowercase());
 | 
				
			||||||
 | 
					    Some(match field {
 | 
				
			||||||
 | 
					        "uid" => "user_id",
 | 
				
			||||||
 | 
					        "mail" => "email",
 | 
				
			||||||
 | 
					        "cn" | "displayname" => "display_name",
 | 
				
			||||||
 | 
					        "givenname" => "first_name",
 | 
				
			||||||
 | 
					        "sn" => "last_name",
 | 
				
			||||||
 | 
					        "avatar" => "avatar",
 | 
				
			||||||
 | 
					        "creationdate" | "createtimestamp" | "modifytimestamp" => "creation_date",
 | 
				
			||||||
 | 
					        "entryuuid" => "uuid",
 | 
				
			||||||
 | 
					        _ => return None,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct LdapInfo {
 | 
				
			||||||
 | 
					    pub base_dn: Vec<(String, String)>,
 | 
				
			||||||
 | 
					    pub base_dn_str: String,
 | 
				
			||||||
 | 
					    pub ignored_user_attributes: Vec<String>,
 | 
				
			||||||
 | 
					    pub ignored_group_attributes: Vec<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
pub mod error;
 | 
					pub mod error;
 | 
				
			||||||
pub mod handler;
 | 
					pub mod handler;
 | 
				
			||||||
 | 
					pub mod ldap;
 | 
				
			||||||
pub mod opaque_handler;
 | 
					pub mod opaque_handler;
 | 
				
			||||||
pub mod sql_backend_handler;
 | 
					pub mod sql_backend_handler;
 | 
				
			||||||
pub mod sql_migrations;
 | 
					pub mod sql_migrations;
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user