Skip to content

Commit 5eccd60

Browse files
committed
Release v0.1.1: Fix undefined attribute handling for OpenLDAP compatibility
1 parent 90a2f5e commit 5eccd60

9 files changed

Lines changed: 501 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.1] - 2025-06-16
9+
10+
### Fixed
11+
- **OpenLDAP Compatibility**: Search filters with undefined attributes now return `UndefinedAttributeType` error (code 17) instead of silently returning 0 results
12+
- Matches OpenLDAP's behavior for better compatibility with existing LDAP client code
13+
- Enables exception-based fallback patterns used for Active Directory/OpenLDAP compatibility
14+
- Added comprehensive tests for undefined attribute handling
15+
16+
### Changed
17+
- Search operations now validate all attributes referenced in filters before executing the search
18+
- Added methods to collect and validate attributes from both schema and existing entries
19+
820
## [0.1.0] - 2025-06-14
921

1022
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "yamldap"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
edition = "2021"
55
authors = ["Ruben J. Jongejan <ruben.jongejan@gmail.com>"]
66
description = "A lightweight LDAP server that serves directory data from YAML files"

src/directory/storage.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,24 @@ impl Directory {
9999
pub fn entry_exists(&self, dn: &str) -> bool {
100100
self.entries.contains_key(&dn.to_lowercase())
101101
}
102+
103+
/// Get all attributes that exist in any entry in the directory
104+
pub fn get_all_existing_attributes(&self) -> std::collections::HashSet<String> {
105+
let mut attributes = std::collections::HashSet::new();
106+
107+
// Always include standard attributes
108+
attributes.insert("objectclass".to_string());
109+
attributes.insert("dn".to_string());
110+
111+
// Collect attributes from all entries
112+
for entry in self.entries.iter() {
113+
for attr_name in entry.attributes.keys() {
114+
attributes.insert(attr_name.to_lowercase());
115+
}
116+
}
117+
118+
attributes
119+
}
102120
}
103121

104122
#[derive(Debug, Clone, Copy, PartialEq)]

src/ldap/filters.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,39 @@ impl LdapFilter {
9393
LdapFilter::Not(filter) => !filter.matches(entry),
9494
}
9595
}
96+
97+
/// Extract all attribute names referenced in this filter
98+
pub fn get_referenced_attributes(&self) -> std::collections::HashSet<String> {
99+
let mut attributes = std::collections::HashSet::new();
100+
self.collect_attributes(&mut attributes);
101+
attributes
102+
}
103+
104+
fn collect_attributes(&self, attributes: &mut std::collections::HashSet<String>) {
105+
match self {
106+
LdapFilter::Present(attr)
107+
| LdapFilter::Equality(attr, _)
108+
| LdapFilter::Substring(attr, _)
109+
| LdapFilter::GreaterOrEqual(attr, _)
110+
| LdapFilter::LessOrEqual(attr, _)
111+
| LdapFilter::Approximate(attr, _) => {
112+
attributes.insert(attr.to_lowercase());
113+
}
114+
LdapFilter::Extensible(ext) => {
115+
if let Some(attr) = &ext.attribute {
116+
attributes.insert(attr.to_lowercase());
117+
}
118+
}
119+
LdapFilter::And(filters) | LdapFilter::Or(filters) => {
120+
for filter in filters {
121+
filter.collect_attributes(attributes);
122+
}
123+
}
124+
LdapFilter::Not(filter) => {
125+
filter.collect_attributes(attributes);
126+
}
127+
}
128+
}
96129
}
97130

98131
impl SubstringFilter {

src/ldap/operations.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ pub fn handle_operation(
8484
}
8585
};
8686

87+
// Check if filter references undefined attributes
88+
let filter_attributes = ldap_filter.get_referenced_attributes();
89+
let existing_attributes = directory.get_all_existing_attributes();
90+
91+
for attr in &filter_attributes {
92+
if !existing_attributes.contains(attr) {
93+
responses.push(LdapMessage {
94+
message_id,
95+
protocol_op: LdapProtocolOp::SearchResultDone {
96+
result: LdapResult::error(
97+
LdapResultCode::UndefinedAttributeType,
98+
format!("{}: attribute type undefined", attr),
99+
),
100+
},
101+
});
102+
return responses;
103+
}
104+
}
105+
87106
// Convert scope
88107
let dir_scope = match scope {
89108
SearchScope::BaseObject => DirSearchScope::BaseObject,
@@ -919,4 +938,89 @@ mod tests {
919938
_ => panic!("Expected ExtendedResponse"),
920939
}
921940
}
941+
942+
#[test]
943+
fn test_search_with_undefined_attribute() {
944+
let directory = create_test_directory();
945+
let auth_handler = AuthHandler::new(false);
946+
947+
// Search with undefined attribute should return UndefinedAttributeType error
948+
let operation = LdapOperation::Search {
949+
base_dn: "dc=example,dc=com".to_string(),
950+
scope: SearchScope::WholeSubtree,
951+
filter: "(userPrincipalName=test)".to_string(),
952+
attributes: vec![],
953+
};
954+
955+
let responses = handle_operation(1, operation, &directory, &auth_handler, true);
956+
957+
// Should have 1 response: SearchResultDone with error
958+
assert_eq!(responses.len(), 1);
959+
960+
match &responses[0].protocol_op {
961+
LdapProtocolOp::SearchResultDone { result } => {
962+
assert_eq!(result.result_code, LdapResultCode::UndefinedAttributeType);
963+
assert!(result
964+
.diagnostic_message
965+
.contains("attribute type undefined"));
966+
}
967+
_ => panic!("Expected SearchResultDone"),
968+
}
969+
}
970+
971+
#[test]
972+
fn test_search_with_undefined_attribute_in_complex_filter() {
973+
let directory = create_test_directory();
974+
let auth_handler = AuthHandler::new(false);
975+
976+
// AND filter with undefined attribute
977+
let operation = LdapOperation::Search {
978+
base_dn: "dc=example,dc=com".to_string(),
979+
scope: SearchScope::WholeSubtree,
980+
filter: "(&(uid=user1)(nonExistentAttr=value))".to_string(),
981+
attributes: vec![],
982+
};
983+
984+
let responses = handle_operation(1, operation, &directory, &auth_handler, true);
985+
986+
// Should have 1 response: SearchResultDone with error
987+
assert_eq!(responses.len(), 1);
988+
989+
match &responses[0].protocol_op {
990+
LdapProtocolOp::SearchResultDone { result } => {
991+
assert_eq!(result.result_code, LdapResultCode::UndefinedAttributeType);
992+
assert!(result.diagnostic_message.contains("nonexistentattr"));
993+
assert!(result
994+
.diagnostic_message
995+
.contains("attribute type undefined"));
996+
}
997+
_ => panic!("Expected SearchResultDone"),
998+
}
999+
}
1000+
1001+
#[test]
1002+
fn test_search_with_valid_attributes_still_works() {
1003+
let directory = create_test_directory();
1004+
let auth_handler = AuthHandler::new(false);
1005+
1006+
// Search with valid attribute should work
1007+
let operation = LdapOperation::Search {
1008+
base_dn: "dc=example,dc=com".to_string(),
1009+
scope: SearchScope::WholeSubtree,
1010+
filter: "(uid=user1)".to_string(),
1011+
attributes: vec![],
1012+
};
1013+
1014+
let responses = handle_operation(1, operation, &directory, &auth_handler, true);
1015+
1016+
// Should have 2 responses: 1 entry + done
1017+
assert_eq!(responses.len(), 2);
1018+
1019+
match &responses[1].protocol_op {
1020+
LdapProtocolOp::SearchResultDone { result } => {
1021+
assert_eq!(result.result_code, LdapResultCode::Success);
1022+
}
1023+
_ => panic!("Expected SearchResultDone"),
1024+
}
1025+
}
9221026
}

src/yaml/schema.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,31 @@ impl From<SchemaConfig> for YamlSchema {
6464
}
6565
}
6666

67+
impl YamlSchema {
68+
/// Get all known attributes from the schema
69+
pub fn get_all_known_attributes(&self) -> std::collections::HashSet<String> {
70+
let mut attributes = std::collections::HashSet::new();
71+
72+
// Add standard LDAP attributes that are always available
73+
attributes.insert("objectclass".to_string());
74+
attributes.insert("dn".to_string());
75+
76+
// Add attributes from all object classes
77+
for attrs in self.object_classes.values() {
78+
for attr in attrs {
79+
attributes.insert(attr.to_lowercase());
80+
}
81+
}
82+
83+
// Add custom attributes
84+
for attr_name in self.custom_attributes.keys() {
85+
attributes.insert(attr_name.to_lowercase());
86+
}
87+
88+
attributes
89+
}
90+
}
91+
6792
impl Default for YamlSchema {
6893
fn default() -> Self {
6994
let mut object_classes = HashMap::new();

tests/integration/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod basic_test;
22
mod complex_filters_test;
33
mod concurrent_test;
4-
mod enhanced_filters_test;
4+
mod enhanced_filters_test;
5+
mod undefined_attributes_test;

0 commit comments

Comments
 (0)