mirror of
https://github.com/lldap/lldap.git
synced 2026-03-31 15:07:48 +01:00
e5c28a61d9
Added logic to return LdapResultCode::NoSuchObject (error 32) when a base scope search returns no results, instead of returning Success with zero entries. This aligns with RFC 4511 LDAP specification.
1531 lines
66 KiB
Rust
1531 lines
66 KiB
Rust
use crate::core::{
|
|
error::{LdapError, LdapResult},
|
|
group::{convert_groups_to_ldap_op, get_groups_list},
|
|
user::{convert_users_to_ldap_op, get_user_list},
|
|
utils::{LdapInfo, LdapSchemaDescription, is_subtree, parse_distinguished_name},
|
|
};
|
|
use chrono::Utc;
|
|
use ldap3_proto::{
|
|
LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, LdapSearchScope,
|
|
proto::{
|
|
LdapDerefAliases, LdapOp, LdapResult as LdapResultOp, LdapSearchRequest,
|
|
OID_PASSWORD_MODIFY, OID_WHOAMI,
|
|
},
|
|
};
|
|
use lldap_access_control::UserAndGroupListerBackendHandler;
|
|
use lldap_domain::{
|
|
public_schema::PublicSchema,
|
|
types::{Group, UserAndGroups},
|
|
};
|
|
use tracing::{debug, warn};
|
|
|
|
#[derive(Debug)]
|
|
enum SearchScope {
|
|
Global,
|
|
Users,
|
|
Groups,
|
|
User(LdapFilter),
|
|
Group(LdapFilter),
|
|
UserOuOnly,
|
|
GroupOuOnly,
|
|
Unknown,
|
|
Invalid,
|
|
}
|
|
|
|
enum InternalSearchResults {
|
|
UsersAndGroups(Vec<UserAndGroups>, Vec<Group>),
|
|
Raw(Vec<LdapOp>),
|
|
Empty,
|
|
}
|
|
|
|
fn get_search_scope(
|
|
base_dn: &[(String, String)],
|
|
dn_parts: &[(String, String)],
|
|
ldap_scope: &LdapSearchScope,
|
|
) -> SearchScope {
|
|
let base_dn_len = base_dn.len();
|
|
if !is_subtree(dn_parts, base_dn) {
|
|
SearchScope::Invalid
|
|
} else if dn_parts.len() == base_dn_len {
|
|
SearchScope::Global
|
|
} else if dn_parts.len() == base_dn_len + 1
|
|
&& dn_parts[0] == ("ou".to_string(), "people".to_string())
|
|
{
|
|
if matches!(ldap_scope, LdapSearchScope::Base) {
|
|
SearchScope::UserOuOnly
|
|
} else {
|
|
SearchScope::Users
|
|
}
|
|
} else if dn_parts.len() == base_dn_len + 1
|
|
&& dn_parts[0] == ("ou".to_string(), "groups".to_string())
|
|
{
|
|
if matches!(ldap_scope, LdapSearchScope::Base) {
|
|
SearchScope::GroupOuOnly
|
|
} else {
|
|
SearchScope::Groups
|
|
}
|
|
} else if dn_parts.len() == base_dn_len + 2
|
|
&& dn_parts[1] == ("ou".to_string(), "people".to_string())
|
|
{
|
|
SearchScope::User(LdapFilter::Equality(
|
|
dn_parts[0].0.clone(),
|
|
dn_parts[0].1.clone(),
|
|
))
|
|
} else if dn_parts.len() == base_dn_len + 2
|
|
&& dn_parts[1] == ("ou".to_string(), "groups".to_string())
|
|
{
|
|
SearchScope::Group(LdapFilter::Equality(
|
|
dn_parts[0].0.clone(),
|
|
dn_parts[0].1.clone(),
|
|
))
|
|
} else {
|
|
SearchScope::Unknown
|
|
}
|
|
}
|
|
|
|
pub(crate) fn make_search_request<S: Into<String>>(
|
|
base: &str,
|
|
filter: LdapFilter,
|
|
attrs: Vec<S>,
|
|
) -> LdapSearchRequest {
|
|
LdapSearchRequest {
|
|
base: base.to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
aliases: LdapDerefAliases::Never,
|
|
sizelimit: 0,
|
|
timelimit: 0,
|
|
typesonly: false,
|
|
filter,
|
|
attrs: attrs.into_iter().map(Into::into).collect(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn make_search_success() -> LdapOp {
|
|
make_search_error(LdapResultCode::Success, "".to_string())
|
|
}
|
|
|
|
pub(crate) fn make_search_error(code: LdapResultCode, message: String) -> LdapOp {
|
|
LdapOp::SearchResultDone(LdapResultOp {
|
|
code,
|
|
matcheddn: "".to_string(),
|
|
message,
|
|
referral: vec![],
|
|
})
|
|
}
|
|
|
|
pub(crate) fn root_dse_response(base_dn: &str) -> LdapOp {
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "".to_string(),
|
|
attributes: vec![
|
|
LdapPartialAttribute {
|
|
atype: "objectClass".to_string(),
|
|
vals: vec![b"top".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "vendorName".to_string(),
|
|
vals: vec![b"LLDAP".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "vendorVersion".to_string(),
|
|
vals: vec![
|
|
concat!("lldap_", env!("CARGO_PKG_VERSION"))
|
|
.to_string()
|
|
.into_bytes(),
|
|
],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "supportedLDAPVersion".to_string(),
|
|
vals: vec![b"3".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "supportedExtension".to_string(),
|
|
vals: vec![
|
|
OID_PASSWORD_MODIFY.as_bytes().to_vec(),
|
|
OID_WHOAMI.as_bytes().to_vec(),
|
|
],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "supportedControl".to_string(),
|
|
vals: vec![],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "supportedFeatures".to_string(),
|
|
// Attribute "+"
|
|
vals: vec![b"1.3.6.1.4.1.4203.1.5.1".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "defaultNamingContext".to_string(),
|
|
vals: vec![base_dn.to_string().into_bytes()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "namingContexts".to_string(),
|
|
vals: vec![base_dn.to_string().into_bytes()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "isGlobalCatalogReady".to_string(),
|
|
vals: vec![b"false".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "subschemaSubentry".to_string(),
|
|
vals: vec![b"cn=Subschema".to_vec()],
|
|
},
|
|
],
|
|
})
|
|
}
|
|
|
|
pub fn make_ldap_subschema_entry(schema: PublicSchema) -> LdapOp {
|
|
let ldap_schema_description: LdapSchemaDescription = LdapSchemaDescription::from(schema);
|
|
let current_time_utc = Utc::now().format("%Y%m%d%H%M%SZ").to_string().into_bytes();
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "cn=Subschema".to_string(),
|
|
attributes: vec![
|
|
LdapPartialAttribute {
|
|
atype: "structuralObjectClass".to_string(),
|
|
vals: vec![b"subentry".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "objectClass".to_string(),
|
|
vals: vec![b"top".to_vec(), b"subentry".to_vec(), b"subschema".to_vec(), b"extensibleObject".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "cn".to_string(),
|
|
vals: vec![b"Subschema".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "createTimestamp".to_string(),
|
|
vals: vec![current_time_utc.to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "modifyTimestamp".to_string(),
|
|
vals: vec![current_time_utc.to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "ldapSyntaxes".to_string(),
|
|
vals: vec![
|
|
b"( 1.3.6.1.1.16.1 DESC 'UUID' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.3 DESC 'Attribute Type Description' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.12 DESC 'Distinguished Name' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.34 DESC 'Name And Optional UID' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.37 DESC 'Object Class Description' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.38 DESC 'OID' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.54 DESC 'LDAP Syntax Description' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.58 DESC 'Substring Assertion' )".to_vec(),
|
|
],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "matchingRules".to_string(),
|
|
vals: vec![
|
|
b"( 1.3.6.1.1.16.2 NAME 'UUIDMatch' SYNTAX 1.3.6.1.1.16.1 )".to_vec(),
|
|
b"( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )".to_vec(),
|
|
b"( 2.5.13.0 NAME 'objectIdentifierMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
|
|
b"( 2.5.13.1 NAME 'distinguishedNameMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )".to_vec(),
|
|
b"( 2.5.13.2 NAME 'caseIgnoreMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
|
|
b"( 2.5.13.4 NAME 'caseIgnoreSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )".to_vec(),
|
|
b"( 2.5.13.23 NAME 'uniqueMemberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )".to_vec(),
|
|
b"( 2.5.13.27 NAME 'generalizedTimeMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
|
|
b"( 2.5.13.28 NAME 'generalizedTimeOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
|
|
b"( 2.5.13.30 NAME 'objectIdentifierFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
|
|
],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "attributeTypes".to_string(),
|
|
vals: {
|
|
let hardcoded_attributes = [
|
|
b"( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' 'user_id' ) DESC 'RFC4519: user identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} SINGLE-VALUE NO-USER-MODIFICATION )".to_vec(),
|
|
b"( 1.2.840.113556.1.2.102 NAME 'memberOf' DESC 'Group that the entry belongs to' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 NO-USER-MODIFICATION USAGE dSAOperation X-ORIGIN 'iPlanet Delegated Administrator' )".to_vec(),
|
|
b"( 1.3.6.1.1.16.4 NAME ( 'entryUUID' 'uuid' ) DESC 'UUID of the entry' EQUALITY UUIDMatch ORDERING UUIDOrderingMatch SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC4512: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
|
|
b"( 2.5.4.3 NAME ( 'cn' 'commonName' 'display_name' ) DESC 'RFC4519: common name(s) for which the entity is known by' SUP name SINGLE-VALUE )".to_vec(),
|
|
b"( 2.5.4.4 NAME ( 'sn' 'surname' 'last_name' ) DESC 'RFC2256: last (family) name(s) for which the entity is known by' SUP name SINGLE-VALUE )".to_vec(),
|
|
b"( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) DESC 'RFC2256: organizational unit this object belongs to' SUP name )".to_vec(),
|
|
b"( 2.5.4.41 NAME 'name' DESC 'RFC4519: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )".to_vec(),
|
|
b"( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC4519: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )".to_vec(),
|
|
b"( 2.5.4.50 NAME ( 'uniqueMember' 'member' ) DESC 'RFC2256: unique member of a group' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )".to_vec(),
|
|
b"( 2.5.18.1 NAME ( 'createTimestamp' 'creation_date' ) DESC 'RFC4512: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC4512: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC4512: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.21.6 NAME 'objectClasses' DESC 'RFC4512: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.21.9 NAME 'structuralObjectClass' DESC 'RFC4512: structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
|
|
b"( 10.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
|
|
b"( 10.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(),
|
|
b"( 10.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
|
|
b"( 10.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
|
|
];
|
|
hardcoded_attributes.into_iter().chain(
|
|
ldap_schema_description
|
|
.formatted_attribute_list(
|
|
4, // The number of hardcoded attributes starting with "10." (LLDAP custom range)
|
|
vec!["creation_date", "display_name", "last_name", "user_id", "uuid"]
|
|
)
|
|
).collect()
|
|
}
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "objectClasses".to_string(),
|
|
vals: vec![
|
|
format!(
|
|
"( 3.0 NAME ( {} ) DESC 'LLDAP builtin: a person' STRUCTURAL MUST ( {} ) MAY ( {} ) )",
|
|
ldap_schema_description.user_object_classes().format_for_ldap_schema_description(),
|
|
ldap_schema_description.required_user_attributes().format_for_ldap_schema_description(),
|
|
ldap_schema_description.optional_user_attributes().format_for_ldap_schema_description(),
|
|
).into_bytes(),
|
|
format!(
|
|
"( 3.1 NAME ( {} ) DESC 'LLDAP builtin: a group' STRUCTURAL MUST ( {} ) MAY ( {} ) )",
|
|
ldap_schema_description.group_object_classes().format_for_ldap_schema_description(),
|
|
ldap_schema_description.required_group_attributes().format_for_ldap_schema_description(),
|
|
ldap_schema_description.optional_group_attributes().format_for_ldap_schema_description(),
|
|
).into_bytes(),
|
|
],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "subschemaSubentry".to_string(),
|
|
vals: vec![b"cn=Subschema".to_vec()],
|
|
},
|
|
],
|
|
})
|
|
}
|
|
pub(crate) fn is_root_dse_request(request: &LdapSearchRequest) -> bool {
|
|
request.base.is_empty()
|
|
&& request.scope == LdapSearchScope::Base
|
|
&& matches!(&request.filter, LdapFilter::Present(attr) if attr.eq_ignore_ascii_case("objectclass"))
|
|
}
|
|
|
|
pub(crate) fn is_subschema_entry_request(request: &LdapSearchRequest) -> bool {
|
|
request.base == "cn=Subschema" && request.scope == LdapSearchScope::Base
|
|
}
|
|
|
|
async fn do_search_internal(
|
|
ldap_info: &LdapInfo,
|
|
backend_handler: &impl UserAndGroupListerBackendHandler,
|
|
request: &LdapSearchRequest,
|
|
schema: &PublicSchema,
|
|
) -> LdapResult<InternalSearchResults> {
|
|
let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?;
|
|
let scope = get_search_scope(&ldap_info.base_dn, &dn_parts, &request.scope);
|
|
debug!(?request.base, ?scope);
|
|
// Disambiguate the lifetimes.
|
|
fn cast<'a, T, R>(x: T) -> T
|
|
where
|
|
T: Fn(&'a LdapFilter) -> R + 'a,
|
|
{
|
|
x
|
|
}
|
|
|
|
let get_user_list = cast(async |filter: &LdapFilter| {
|
|
let need_groups = request
|
|
.attrs
|
|
.iter()
|
|
.any(|s| s.eq_ignore_ascii_case("memberof"));
|
|
get_user_list(
|
|
ldap_info,
|
|
filter,
|
|
need_groups,
|
|
&request.base,
|
|
backend_handler,
|
|
schema,
|
|
)
|
|
.await
|
|
});
|
|
let get_group_list = cast(|filter: &LdapFilter| async {
|
|
get_groups_list(ldap_info, filter, &request.base, backend_handler, schema).await
|
|
});
|
|
Ok(match scope {
|
|
SearchScope::Global => {
|
|
let users = get_user_list(&request.filter).await;
|
|
let groups = get_group_list(&request.filter).await;
|
|
match (users, groups) {
|
|
(Ok(users), Err(e)) => {
|
|
warn!("Error while getting groups: {:#}", e);
|
|
InternalSearchResults::UsersAndGroups(users, Vec::new())
|
|
}
|
|
(Err(e), Ok(groups)) => {
|
|
warn!("Error while getting users: {:#}", e);
|
|
InternalSearchResults::UsersAndGroups(Vec::new(), groups)
|
|
}
|
|
(Err(user_error), Err(_)) => InternalSearchResults::Raw(vec![make_search_error(
|
|
user_error.code,
|
|
user_error.message,
|
|
)]),
|
|
(Ok(users), Ok(groups)) => InternalSearchResults::UsersAndGroups(users, groups),
|
|
}
|
|
}
|
|
SearchScope::Users => {
|
|
InternalSearchResults::UsersAndGroups(get_user_list(&request.filter).await?, Vec::new())
|
|
}
|
|
SearchScope::Groups => InternalSearchResults::UsersAndGroups(
|
|
Vec::new(),
|
|
get_group_list(&request.filter).await?,
|
|
),
|
|
SearchScope::User(filter) => {
|
|
let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
|
|
InternalSearchResults::UsersAndGroups(get_user_list(&filter).await?, Vec::new())
|
|
}
|
|
SearchScope::Group(filter) => {
|
|
let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
|
|
InternalSearchResults::UsersAndGroups(Vec::new(), get_group_list(&filter).await?)
|
|
}
|
|
SearchScope::UserOuOnly | SearchScope::GroupOuOnly => {
|
|
InternalSearchResults::Raw(vec![LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: request.base.clone(),
|
|
attributes: vec![LdapPartialAttribute {
|
|
atype: "objectClass".to_owned(),
|
|
vals: vec![b"top".to_vec(), b"organizationalUnit".to_vec()],
|
|
}],
|
|
})])
|
|
}
|
|
SearchScope::Unknown => {
|
|
warn!(
|
|
r#"The requested search tree "{}" matches neither the user subtree "ou=people,{}" nor the group subtree "ou=groups,{}""#,
|
|
&request.base, &ldap_info.base_dn_str, &ldap_info.base_dn_str
|
|
);
|
|
InternalSearchResults::Empty
|
|
}
|
|
SearchScope::Invalid => {
|
|
// Search path is not in our tree, just return an empty success.
|
|
warn!(
|
|
"The specified search tree {:?} is not under the common subtree {:?}",
|
|
&dn_parts, &ldap_info.base_dn
|
|
);
|
|
InternalSearchResults::Empty
|
|
}
|
|
})
|
|
}
|
|
|
|
pub async fn do_search(
|
|
backend_handler: &impl UserAndGroupListerBackendHandler,
|
|
ldap_info: &LdapInfo,
|
|
request: &LdapSearchRequest,
|
|
) -> LdapResult<Vec<LdapOp>> {
|
|
let schema = PublicSchema::from(backend_handler.get_schema().await.map_err(|e| LdapError {
|
|
code: LdapResultCode::OperationsError,
|
|
message: format!("Unable to get schema: {e:#}"),
|
|
})?);
|
|
let search_results = do_search_internal(ldap_info, backend_handler, request, &schema).await?;
|
|
let mut results = match search_results {
|
|
InternalSearchResults::UsersAndGroups(users, groups) => {
|
|
convert_users_to_ldap_op(users, &request.attrs, ldap_info, &schema)
|
|
.chain(convert_groups_to_ldap_op(
|
|
groups,
|
|
&request.attrs,
|
|
ldap_info,
|
|
backend_handler.user_filter(),
|
|
&schema,
|
|
))
|
|
.collect()
|
|
}
|
|
InternalSearchResults::Raw(raw_results) => raw_results,
|
|
InternalSearchResults::Empty => Vec::new(),
|
|
};
|
|
// RFC 4511: When performing a base scope search, if the entry doesn't exist,
|
|
// we should return NoSuchObject instead of Success with zero entries
|
|
if results.is_empty() && request.scope == LdapSearchScope::Base {
|
|
return Err(LdapError {
|
|
code: LdapResultCode::NoSuchObject,
|
|
message: "".to_string(),
|
|
});
|
|
}
|
|
if !matches!(results.last(), Some(LdapOp::SearchResultDone(_))) {
|
|
results.push(make_search_success());
|
|
}
|
|
Ok(results)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
core::error::LdapError,
|
|
handler::tests::{
|
|
make_group_search_request, make_user_search_request, setup_bound_admin_handler,
|
|
setup_bound_readonly_handler,
|
|
},
|
|
};
|
|
use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc};
|
|
use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope, LdapSubstringFilter};
|
|
use lldap_domain::{
|
|
schema::{AttributeList, AttributeSchema, Schema},
|
|
types::{
|
|
Attribute, AttributeName, AttributeType, GroupId, JpegPhoto, LdapObjectClass, User,
|
|
UserId,
|
|
},
|
|
uuid,
|
|
};
|
|
use lldap_domain_handlers::handler::*;
|
|
use lldap_domain_model::model::UserColumn;
|
|
use lldap_test_utils::MockTestBackendHandler;
|
|
use mockall::predicate::eq;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
fn assert_timestamp_within_margin(
|
|
timestamp_bytes: &[u8],
|
|
base_timestamp_dt: DateTime<Utc>,
|
|
time_margin: Duration,
|
|
) {
|
|
let timestamp_str =
|
|
std::str::from_utf8(timestamp_bytes).expect("Invalid conversion from UTF-8 to string");
|
|
let timestamp_naive = NaiveDateTime::parse_from_str(timestamp_str, "%Y%m%d%H%M%SZ")
|
|
.expect("Invalid timestamp format");
|
|
let timestamp_dt: DateTime<Utc> = Utc.from_utc_datetime(×tamp_naive);
|
|
|
|
let within_range = (base_timestamp_dt - timestamp_dt).abs() <= time_margin;
|
|
|
|
assert!(
|
|
within_range,
|
|
"Timestamp not within range: expected within [{} - {}], got [{}]",
|
|
base_timestamp_dt - time_margin,
|
|
base_timestamp_dt + time_margin,
|
|
timestamp_dt
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_root_dse() {
|
|
let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
|
|
let request = LdapSearchRequest {
|
|
base: "".to_string(),
|
|
scope: LdapSearchScope::Base,
|
|
aliases: LdapDerefAliases::Never,
|
|
sizelimit: 0,
|
|
timelimit: 0,
|
|
typesonly: false,
|
|
filter: LdapFilter::Present("objectClass".to_string()),
|
|
attrs: vec!["supportedExtension".to_string()],
|
|
};
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![
|
|
root_dse_response("dc=example,dc=com"),
|
|
make_search_success()
|
|
])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_subschema_response() {
|
|
let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
|
|
|
|
let request = LdapSearchRequest {
|
|
base: "cn=Subschema".to_string(),
|
|
scope: LdapSearchScope::Base,
|
|
aliases: LdapDerefAliases::Never,
|
|
sizelimit: 0,
|
|
timelimit: 0,
|
|
typesonly: false,
|
|
filter: LdapFilter::Present("objectClass".to_string()),
|
|
attrs: vec!["supportedExtension".to_string()],
|
|
};
|
|
|
|
let actual_reponse: Vec<LdapOp> = ldap_handler.do_search_or_dse(&request).await.unwrap();
|
|
|
|
let LdapOp::SearchResultEntry(search_result_entry) = &actual_reponse[0] else {
|
|
panic!("Expected SearchResultEntry");
|
|
};
|
|
|
|
let attrs = &search_result_entry.attributes;
|
|
assert_eq!(attrs.len(), 10);
|
|
assert_eq!(search_result_entry.dn, "cn=Subschema".to_owned());
|
|
|
|
assert_eq!(
|
|
attrs[0],
|
|
LdapPartialAttribute {
|
|
atype: "structuralObjectClass".to_owned(),
|
|
vals: vec![b"subentry".to_vec()]
|
|
}
|
|
);
|
|
|
|
assert_eq!(
|
|
attrs[1],
|
|
LdapPartialAttribute {
|
|
atype: "objectClass".to_owned(),
|
|
vals: vec![
|
|
b"top".to_vec(),
|
|
b"subentry".to_vec(),
|
|
b"subschema".to_vec(),
|
|
b"extensibleObject".to_vec()
|
|
]
|
|
}
|
|
);
|
|
|
|
assert_eq!(
|
|
attrs[2],
|
|
LdapPartialAttribute {
|
|
atype: "cn".to_owned(),
|
|
vals: vec![b"Subschema".to_vec()]
|
|
}
|
|
);
|
|
|
|
let check_timestamp_attribute = |attr: &LdapPartialAttribute, expected_type: &str| {
|
|
assert_eq!(attr.atype, expected_type);
|
|
assert_eq!(attr.vals.len(), 1);
|
|
assert_timestamp_within_margin(&attr.vals[0], Utc::now(), Duration::seconds(300));
|
|
};
|
|
check_timestamp_attribute(&attrs[3], "createTimestamp");
|
|
check_timestamp_attribute(&attrs[4], "modifyTimestamp");
|
|
|
|
assert_eq!(
|
|
attrs[5],
|
|
LdapPartialAttribute {
|
|
atype: "ldapSyntaxes".to_owned(),
|
|
vals: vec![
|
|
b"( 1.3.6.1.1.16.1 DESC 'UUID' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.3 DESC 'Attribute Type Description' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.12 DESC 'Distinguished Name' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )"
|
|
.to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.34 DESC 'Name And Optional UID' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.37 DESC 'Object Class Description' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.38 DESC 'OID' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.54 DESC 'LDAP Syntax Description' )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.115.121.1.58 DESC 'Substring Assertion' )".to_vec(),
|
|
]
|
|
}
|
|
);
|
|
|
|
assert_eq!(
|
|
attrs[6],
|
|
LdapPartialAttribute {
|
|
atype: "matchingRules".to_string(),
|
|
vals: vec![
|
|
b"( 1.3.6.1.1.16.2 NAME 'UUIDMatch' SYNTAX 1.3.6.1.1.16.1 )".to_vec(),
|
|
b"( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )".to_vec(),
|
|
b"( 2.5.13.0 NAME 'objectIdentifierMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
|
|
b"( 2.5.13.1 NAME 'distinguishedNameMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )".to_vec(),
|
|
b"( 2.5.13.2 NAME 'caseIgnoreMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
|
|
b"( 2.5.13.4 NAME 'caseIgnoreSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )".to_vec(),
|
|
b"( 2.5.13.23 NAME 'uniqueMemberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )".to_vec(),
|
|
b"( 2.5.13.27 NAME 'generalizedTimeMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
|
|
b"( 2.5.13.28 NAME 'generalizedTimeOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
|
|
b"( 2.5.13.30 NAME 'objectIdentifierFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
|
|
]
|
|
}
|
|
);
|
|
|
|
assert_eq!(
|
|
attrs[7],
|
|
LdapPartialAttribute {
|
|
atype: "attributeTypes".to_owned(),
|
|
vals: vec![
|
|
b"( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' 'user_id' ) DESC 'RFC4519: user identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} SINGLE-VALUE NO-USER-MODIFICATION )".to_vec(),
|
|
b"( 1.2.840.113556.1.2.102 NAME 'memberOf' DESC 'Group that the entry belongs to' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 NO-USER-MODIFICATION USAGE dSAOperation X-ORIGIN 'iPlanet Delegated Administrator' )".to_vec(),
|
|
b"( 1.3.6.1.1.16.4 NAME ( 'entryUUID' 'uuid' ) DESC 'UUID of the entry' EQUALITY UUIDMatch ORDERING UUIDOrderingMatch SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
|
|
b"( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC4512: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
|
|
b"( 2.5.4.3 NAME ( 'cn' 'commonName' 'display_name' ) DESC 'RFC4519: common name(s) for which the entity is known by' SUP name SINGLE-VALUE )".to_vec(),
|
|
b"( 2.5.4.4 NAME ( 'sn' 'surname' 'last_name' ) DESC 'RFC2256: last (family) name(s) for which the entity is known by' SUP name SINGLE-VALUE )".to_vec(),
|
|
b"( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) DESC 'RFC2256: organizational unit this object belongs to' SUP name )".to_vec(),
|
|
b"( 2.5.4.41 NAME 'name' DESC 'RFC4519: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )".to_vec(),
|
|
b"( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC4519: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )".to_vec(),
|
|
b"( 2.5.4.50 NAME ( 'uniqueMember' 'member' ) DESC 'RFC2256: unique member of a group' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )".to_vec(),
|
|
b"( 2.5.18.1 NAME ( 'createTimestamp' 'creation_date' ) DESC 'RFC4512: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC4512: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC4512: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.21.6 NAME 'objectClasses' DESC 'RFC4512: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )".to_vec(),
|
|
b"( 2.5.21.9 NAME 'structuralObjectClass' DESC 'RFC4512: structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
|
|
b"( 10.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
|
|
b"( 10.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(),
|
|
b"( 10.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
|
|
b"( 10.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
|
|
b"( 10.4 NAME 'avatar' DESC 'LLDAP: builtin attribute' SUP JpegPhoto )".to_vec(),
|
|
b"( 10.5 NAME 'first_name' DESC 'LLDAP: builtin attribute' SUP String )"
|
|
.to_vec(),
|
|
b"( 10.6 NAME 'mail' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
|
|
b"( 10.7 NAME 'modified_date' DESC 'LLDAP: builtin attribute' SUP DateTime )".to_vec(),
|
|
b"( 10.8 NAME 'password_modified_date' DESC 'LLDAP: builtin attribute' SUP DateTime )".to_vec(),
|
|
b"( 10.9 NAME 'group_id' DESC 'LLDAP: builtin attribute' SUP Integer )"
|
|
.to_vec(),
|
|
b"( 10.10 NAME 'modified_date' DESC 'LLDAP: builtin attribute' SUP DateTime )".to_vec(),
|
|
]
|
|
}
|
|
);
|
|
|
|
assert_eq!(attrs[8],
|
|
LdapPartialAttribute {
|
|
atype: "objectClasses".to_owned(),
|
|
vals: vec![
|
|
b"( 3.0 NAME ( 'inetOrgPerson' 'posixAccount' 'mailAccount' 'person' 'customUserClass' ) DESC 'LLDAP builtin: a person' STRUCTURAL MUST ( mail $ user_id ) MAY ( avatar $ creation_date $ display_name $ first_name $ last_name $ modified_date $ password_modified_date $ uuid ) )".to_vec(),
|
|
b"( 3.1 NAME ( 'groupOfUniqueNames' 'groupOfNames' ) DESC 'LLDAP builtin: a group' STRUCTURAL MUST ( display_name ) MAY ( creation_date $ group_id $ modified_date $ uuid ) )".to_vec(),
|
|
]
|
|
}
|
|
);
|
|
|
|
assert_eq!(
|
|
attrs[9],
|
|
LdapPartialAttribute {
|
|
atype: "subschemaSubentry".to_owned(),
|
|
vals: vec![b"cn=Subschema".to_vec()]
|
|
}
|
|
);
|
|
|
|
assert_eq!(actual_reponse[1], make_search_success());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_groups_unsupported_substring() {
|
|
let ldap_handler = setup_bound_readonly_handler(MockTestBackendHandler::new()).await;
|
|
let request = make_group_search_request(
|
|
LdapFilter::Substring("member".to_owned(), LdapSubstringFilter::default()),
|
|
vec!["cn"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Err(LdapError {
|
|
code: LdapResultCode::UnwillingToPerform,
|
|
message: r#"Unsupported group attribute for substring filter: "member""#.to_owned()
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_groups_missing_attribute_substring() {
|
|
let request = make_group_search_request(
|
|
LdapFilter::Substring("nonexistent".to_owned(), LdapSubstringFilter::default()),
|
|
vec!["cn"],
|
|
);
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_groups()
|
|
.with(eq(Some(false.into())))
|
|
.times(1)
|
|
.return_once(|_| Ok(vec![]));
|
|
let ldap_handler = setup_bound_readonly_handler(mock).await;
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![make_search_success()]),
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_groups_error() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_groups()
|
|
.with(eq(Some(GroupRequestFilter::Not(Box::new(
|
|
GroupRequestFilter::DisplayName("group_2".into()),
|
|
)))))
|
|
.times(1)
|
|
.return_once(|_| {
|
|
Err(lldap_domain_model::error::DomainError::InternalError(
|
|
"Error getting groups".to_string(),
|
|
))
|
|
});
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_group_search_request(
|
|
LdapFilter::Or(vec![LdapFilter::Not(Box::new(LdapFilter::Equality(
|
|
"displayname".to_string(),
|
|
"group_2".to_string(),
|
|
)))]),
|
|
vec!["cn"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Err(LdapError{
|
|
code: LdapResultCode::Other,
|
|
message: r#"Error while listing groups "ou=groups,dc=example,dc=com": Internal error: `Error getting groups`"#.to_string()
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_groups_filter_error() {
|
|
let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
|
|
let request = make_group_search_request(
|
|
LdapFilter::And(vec![LdapFilter::Approx(
|
|
"whatever".to_owned(),
|
|
"value".to_owned(),
|
|
)]),
|
|
vec!["cn"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Err(LdapError {
|
|
code: LdapResultCode::UnwillingToPerform,
|
|
message: r#"Unsupported group filter: Approx("whatever", "value")"#.to_string()
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_filters() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users()
|
|
.with(
|
|
eq(Some(UserRequestFilter::Or(vec![
|
|
UserRequestFilter::Not(Box::new(UserRequestFilter::UserId(UserId::new("bob")))),
|
|
UserRequestFilter::UserId("bob_1".to_string().into()),
|
|
true.into(),
|
|
true.into(),
|
|
true.into(),
|
|
UserRequestFilter::AttributeEquality(
|
|
AttributeName::from("first_name"),
|
|
"FirstName".to_string().into(),
|
|
),
|
|
UserRequestFilter::AttributeEquality(
|
|
AttributeName::from("first_name"),
|
|
"firstname".to_string().into(),
|
|
),
|
|
UserRequestFilter::UserIdSubString(SubStringFilter {
|
|
initial: Some("iNIt".to_owned()),
|
|
any: vec!["1".to_owned(), "2aA".to_owned()],
|
|
final_: Some("finAl".to_owned()),
|
|
}),
|
|
UserRequestFilter::SubString(
|
|
UserColumn::DisplayName,
|
|
SubStringFilter {
|
|
initial: Some("iNIt".to_owned()),
|
|
any: vec!["1".to_owned(), "2aA".to_owned()],
|
|
final_: Some("finAl".to_owned()),
|
|
},
|
|
),
|
|
]))),
|
|
eq(false),
|
|
)
|
|
.times(1)
|
|
.return_once(|_, _| Ok(vec![]));
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_user_search_request(
|
|
LdapFilter::And(vec![LdapFilter::Or(vec![
|
|
LdapFilter::Not(Box::new(LdapFilter::Equality(
|
|
"uid".to_string(),
|
|
"bob".to_string(),
|
|
))),
|
|
LdapFilter::Equality(
|
|
"dn".to_string(),
|
|
"uid=bob_1,ou=people,dc=example,dc=com".to_string(),
|
|
),
|
|
LdapFilter::Equality(
|
|
"dn".to_string(),
|
|
"uid=bob_1,ou=groups,dc=example,dc=com".to_string(),
|
|
),
|
|
LdapFilter::Equality("objectclass".to_string(), "persOn".to_string()),
|
|
LdapFilter::Equality("objectclass".to_string(), "other".to_string()),
|
|
LdapFilter::Present("objectClass".to_string()),
|
|
LdapFilter::Present("uid".to_string()),
|
|
LdapFilter::Present("unknown".to_string()),
|
|
LdapFilter::Equality("givenname".to_string(), "FirstName".to_string()),
|
|
LdapFilter::Equality("unknown_attribute".to_string(), "randomValue".to_string()),
|
|
LdapFilter::Substring(
|
|
"uid".to_owned(),
|
|
LdapSubstringFilter {
|
|
initial: Some("iNIt".to_owned()),
|
|
any: vec!["1".to_owned(), "2aA".to_owned()],
|
|
final_: Some("finAl".to_owned()),
|
|
},
|
|
),
|
|
LdapFilter::Substring(
|
|
"displayName".to_owned(),
|
|
LdapSubstringFilter {
|
|
initial: Some("iNIt".to_owned()),
|
|
any: vec!["1".to_owned(), "2aA".to_owned()],
|
|
final_: Some("finAl".to_owned()),
|
|
},
|
|
),
|
|
])]),
|
|
vec!["objectClass"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![make_search_success()])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_unsupported_substring_filter() {
|
|
let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
|
|
let request = make_user_search_request(
|
|
LdapFilter::Substring(
|
|
"uuid".to_owned(),
|
|
LdapSubstringFilter {
|
|
initial: Some("iNIt".to_owned()),
|
|
any: vec!["1".to_owned(), "2aA".to_owned()],
|
|
final_: Some("finAl".to_owned()),
|
|
},
|
|
),
|
|
vec!["objectClass"],
|
|
);
|
|
ldap_handler.do_search_or_dse(&request).await.unwrap_err();
|
|
let request = make_user_search_request(
|
|
LdapFilter::Substring(
|
|
"givenname".to_owned(),
|
|
LdapSubstringFilter {
|
|
initial: Some("iNIt".to_owned()),
|
|
any: vec!["1".to_owned(), "2aA".to_owned()],
|
|
final_: Some("finAl".to_owned()),
|
|
},
|
|
),
|
|
vec!["objectClass"],
|
|
);
|
|
ldap_handler.do_search_or_dse(&request).await.unwrap_err();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_member_of_filter() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users()
|
|
.with(
|
|
eq(Some(UserRequestFilter::MemberOf("group_1".into()))),
|
|
eq(false),
|
|
)
|
|
.times(2)
|
|
.returning(|_, _| Ok(vec![]));
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_user_search_request(
|
|
LdapFilter::Equality(
|
|
"memberOf".to_string(),
|
|
"cn=group_1, ou=groups, dc=example,dc=com".to_string(),
|
|
),
|
|
vec!["objectClass"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![make_search_success()])
|
|
);
|
|
let request = make_user_search_request(
|
|
LdapFilter::Equality("memberOf".to_string(), "group_1".to_string()),
|
|
vec!["objectClass"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![make_search_success()])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_member_of_filter_error() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users()
|
|
.with(eq(Some(UserRequestFilter::from(false))), eq(false))
|
|
.times(1)
|
|
.returning(|_, _| Ok(vec![]));
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_user_search_request(
|
|
LdapFilter::Equality(
|
|
"memberOf".to_string(),
|
|
"cn=mygroup,dc=example,dc=com".to_string(),
|
|
),
|
|
vec!["objectClass"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
// The error is ignored, a warning is printed.
|
|
Ok(vec![make_search_success()])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_filters_lowercase() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users()
|
|
.with(
|
|
eq(Some(UserRequestFilter::Not(Box::new(
|
|
UserRequestFilter::Equality(UserColumn::DisplayName, "bob".to_string()),
|
|
)))),
|
|
eq(false),
|
|
)
|
|
.times(1)
|
|
.return_once(|_, _| {
|
|
Ok(vec![UserAndGroups {
|
|
user: User {
|
|
user_id: UserId::new("bob_1"),
|
|
..Default::default()
|
|
},
|
|
groups: None,
|
|
}])
|
|
});
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_user_search_request(
|
|
LdapFilter::And(vec![LdapFilter::Or(vec![LdapFilter::Not(Box::new(
|
|
LdapFilter::Equality("displayname".to_string(), "bob".to_string()),
|
|
))])]),
|
|
vec!["objectclass"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(),
|
|
attributes: vec![LdapPartialAttribute {
|
|
atype: "objectclass".to_string(),
|
|
vals: vec![
|
|
b"inetOrgPerson".to_vec(),
|
|
b"posixAccount".to_vec(),
|
|
b"mailAccount".to_vec(),
|
|
b"person".to_vec(),
|
|
b"customUserClass".to_vec(),
|
|
]
|
|
},]
|
|
}),
|
|
make_search_success()
|
|
])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_filters_custom_object_class() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users()
|
|
.with(eq(Some(UserRequestFilter::from(true))), eq(false))
|
|
.times(1)
|
|
.return_once(|_, _| {
|
|
Ok(vec![UserAndGroups {
|
|
user: User {
|
|
user_id: UserId::new("bob_1"),
|
|
..Default::default()
|
|
},
|
|
groups: None,
|
|
}])
|
|
});
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_user_search_request(
|
|
LdapFilter::Equality("objectClass".to_owned(), "CUSTOMuserCLASS".to_owned()),
|
|
vec!["objectclass"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(),
|
|
attributes: vec![LdapPartialAttribute {
|
|
atype: "objectclass".to_string(),
|
|
vals: vec![
|
|
b"inetOrgPerson".to_vec(),
|
|
b"posixAccount".to_vec(),
|
|
b"mailAccount".to_vec(),
|
|
b"person".to_vec(),
|
|
b"customUserClass".to_vec(),
|
|
]
|
|
},]
|
|
}),
|
|
make_search_success()
|
|
])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_both() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users().times(1).return_once(|_, _| {
|
|
Ok(vec![UserAndGroups {
|
|
user: User {
|
|
user_id: UserId::new("bob_1"),
|
|
email: "bob@bobmail.bob".into(),
|
|
display_name: Some("Bôb Böbberson".to_string()),
|
|
attributes: vec![
|
|
Attribute {
|
|
name: "first_name".into(),
|
|
value: "Bôb".to_string().into(),
|
|
},
|
|
Attribute {
|
|
name: "last_name".to_string().into(),
|
|
value: "Böbberson".to_string().into(),
|
|
},
|
|
],
|
|
..Default::default()
|
|
},
|
|
groups: None,
|
|
}])
|
|
});
|
|
mock.expect_list_groups()
|
|
.with(eq(Some(GroupRequestFilter::True)))
|
|
.times(1)
|
|
.return_once(|_| {
|
|
Ok(vec![Group {
|
|
id: GroupId(1),
|
|
display_name: "group_1".into(),
|
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
users: vec![UserId::new("bob"), UserId::new("john")],
|
|
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
|
|
attributes: Vec::new(),
|
|
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
}])
|
|
});
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_search_request(
|
|
"dc=example,dc=com",
|
|
LdapFilter::And(vec![]),
|
|
vec!["objectClass", "dn", "cn"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(),
|
|
attributes: vec![
|
|
LdapPartialAttribute {
|
|
atype: "cn".to_string(),
|
|
vals: vec!["Bôb Böbberson".to_string().into_bytes()]
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "objectClass".to_string(),
|
|
vals: vec![
|
|
b"inetOrgPerson".to_vec(),
|
|
b"posixAccount".to_vec(),
|
|
b"mailAccount".to_vec(),
|
|
b"person".to_vec(),
|
|
b"customUserClass".to_vec(),
|
|
],
|
|
},
|
|
],
|
|
}),
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
|
|
attributes: vec![
|
|
LdapPartialAttribute {
|
|
atype: "cn".to_string(),
|
|
vals: vec![b"group_1".to_vec()]
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "objectClass".to_string(),
|
|
vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec(),],
|
|
},
|
|
],
|
|
}),
|
|
make_search_success(),
|
|
])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_wildcards() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
|
|
mock.expect_list_users().returning(|_, _| {
|
|
Ok(vec![UserAndGroups {
|
|
user: User {
|
|
user_id: UserId::new("bob_1"),
|
|
email: "bob@bobmail.bob".into(),
|
|
display_name: Some("Bôb Böbberson".to_string()),
|
|
attributes: vec![
|
|
Attribute {
|
|
name: "avatar".into(),
|
|
value: JpegPhoto::for_tests().into(),
|
|
},
|
|
Attribute {
|
|
name: "last_name".into(),
|
|
value: "Böbberson".to_string().into(),
|
|
},
|
|
],
|
|
uuid: uuid!("b4ac75e0-2900-3e21-926c-2f732c26b3fc"),
|
|
..Default::default()
|
|
},
|
|
groups: None,
|
|
}])
|
|
});
|
|
mock.expect_list_groups()
|
|
.with(eq(Some(GroupRequestFilter::True)))
|
|
.returning(|_| {
|
|
Ok(vec![Group {
|
|
id: GroupId(1),
|
|
display_name: "group_1".into(),
|
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
users: vec![UserId::new("bob"), UserId::new("john")],
|
|
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
|
|
attributes: Vec::new(),
|
|
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
}])
|
|
});
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
|
|
// Test simple wildcard
|
|
let request =
|
|
make_search_request("dc=example,dc=com", LdapFilter::And(vec![]), vec!["*", "+"]);
|
|
|
|
// all: "objectclass", "dn", "uid", "mail", "givenname", "sn", "cn"
|
|
// Operational: "createtimestamp"
|
|
|
|
let expected_result = Ok(vec![
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(),
|
|
attributes: vec![
|
|
LdapPartialAttribute {
|
|
atype: "avatar".to_string(),
|
|
vals: vec![JpegPhoto::for_tests().into_bytes()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "cn".to_string(),
|
|
vals: vec!["Bôb Böbberson".to_string().into_bytes()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "createtimestamp".to_string(),
|
|
vals: vec![b"19700101000000Z".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "entryuuid".to_string(),
|
|
vals: vec![b"b4ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "jpegPhoto".to_string(),
|
|
vals: vec![JpegPhoto::for_tests().into_bytes()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "last_name".to_string(),
|
|
vals: vec!["Böbberson".to_string().into_bytes()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "mail".to_string(),
|
|
vals: vec![b"bob@bobmail.bob".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "objectclass".to_string(),
|
|
vals: vec![
|
|
b"inetOrgPerson".to_vec(),
|
|
b"posixAccount".to_vec(),
|
|
b"mailAccount".to_vec(),
|
|
b"person".to_vec(),
|
|
b"customUserClass".to_vec(),
|
|
],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "sn".to_string(),
|
|
vals: vec!["Böbberson".to_string().into_bytes()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "uid".to_string(),
|
|
vals: vec![b"bob_1".to_vec()],
|
|
},
|
|
],
|
|
}),
|
|
// "objectclass", "dn", "uid", "cn", "member", "uniquemember"
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
|
|
attributes: vec![
|
|
LdapPartialAttribute {
|
|
atype: "cn".to_string(),
|
|
vals: vec![b"group_1".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "entryuuid".to_string(),
|
|
vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()],
|
|
},
|
|
//member / uniquemember : "uid={},ou=people,{}"
|
|
LdapPartialAttribute {
|
|
atype: "member".to_string(),
|
|
vals: vec![
|
|
b"uid=bob,ou=people,dc=example,dc=com".to_vec(),
|
|
b"uid=john,ou=people,dc=example,dc=com".to_vec(),
|
|
],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "objectclass".to_string(),
|
|
vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec()],
|
|
},
|
|
// UID
|
|
LdapPartialAttribute {
|
|
atype: "uid".to_string(),
|
|
vals: vec![b"group_1".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "uniquemember".to_string(),
|
|
vals: vec![
|
|
b"uid=bob,ou=people,dc=example,dc=com".to_vec(),
|
|
b"uid=john,ou=people,dc=example,dc=com".to_vec(),
|
|
],
|
|
},
|
|
],
|
|
}),
|
|
make_search_success(),
|
|
]);
|
|
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
expected_result
|
|
);
|
|
|
|
let request2 = make_search_request(
|
|
"dc=example,dc=com",
|
|
LdapFilter::And(vec![]),
|
|
vec!["objectclass", "obJEctclaSS", "dn", "*", "*"],
|
|
);
|
|
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request2).await,
|
|
expected_result
|
|
);
|
|
|
|
let request3 = make_search_request(
|
|
"dc=example,dc=com",
|
|
LdapFilter::And(vec![]),
|
|
vec!["*", "+", "+"],
|
|
);
|
|
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request3).await,
|
|
expected_result
|
|
);
|
|
|
|
let request4 =
|
|
make_search_request("dc=example,dc=com", LdapFilter::And(vec![]), vec![""; 0]);
|
|
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request4).await,
|
|
expected_result
|
|
);
|
|
|
|
let request5 = make_search_request(
|
|
"dc=example,dc=com",
|
|
LdapFilter::And(vec![]),
|
|
vec!["objectclass", "dn", "uid", "*"],
|
|
);
|
|
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request5).await,
|
|
expected_result
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_wrong_base() {
|
|
let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
|
|
let request = make_search_request(
|
|
"ou=users,dc=example,dc=com",
|
|
LdapFilter::And(vec![]),
|
|
vec!["objectClass"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![make_search_success()])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_unsupported_filters() {
|
|
let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
|
|
let request = make_user_search_request(
|
|
LdapFilter::Approx("uid".to_owned(), "value".to_owned()),
|
|
vec!["objectClass"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Err(LdapError {
|
|
code: LdapResultCode::UnwillingToPerform,
|
|
message: r#"Unsupported user filter: Approx("uid", "value")"#.to_string()
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_filter_non_attribute() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users()
|
|
.with(eq(Some(true.into())), eq(false))
|
|
.times(1)
|
|
.return_once(|_, _| Ok(vec![]));
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = make_user_search_request(
|
|
LdapFilter::Present("displayname".to_owned()),
|
|
vec!["objectClass"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![make_search_success()])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_user_ou_search() {
|
|
let ldap_handler = setup_bound_readonly_handler(MockTestBackendHandler::new()).await;
|
|
let request = LdapSearchRequest {
|
|
base: "ou=people,dc=example,dc=com".to_owned(),
|
|
scope: LdapSearchScope::Base,
|
|
aliases: LdapDerefAliases::Never,
|
|
sizelimit: 0,
|
|
timelimit: 0,
|
|
typesonly: false,
|
|
filter: LdapFilter::And(vec![]),
|
|
attrs: Vec::new(),
|
|
};
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "ou=people,dc=example,dc=com".to_owned(),
|
|
attributes: vec![LdapPartialAttribute {
|
|
atype: "objectClass".to_owned(),
|
|
vals: vec![b"top".to_vec(), b"organizationalUnit".to_vec()]
|
|
}]
|
|
}),
|
|
make_search_success()
|
|
])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_custom_attribute_read() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users().times(1).return_once(|_, _| {
|
|
Ok(vec![UserAndGroups {
|
|
user: User {
|
|
user_id: UserId::new("test"),
|
|
attributes: vec![Attribute {
|
|
name: "nickname".into(),
|
|
value: "Bob the Builder".to_string().into(),
|
|
}],
|
|
..Default::default()
|
|
},
|
|
groups: None,
|
|
}])
|
|
});
|
|
mock.expect_list_groups().times(1).return_once(|_| {
|
|
Ok(vec![Group {
|
|
id: GroupId(1),
|
|
display_name: "group".into(),
|
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
users: vec![UserId::new("bob")],
|
|
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
|
|
attributes: vec![Attribute {
|
|
name: "club_name".into(),
|
|
value: "Breakfast Club".to_string().into(),
|
|
}],
|
|
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
}])
|
|
});
|
|
mock.expect_get_schema().returning(|| {
|
|
Ok(Schema {
|
|
user_attributes: AttributeList {
|
|
attributes: vec![AttributeSchema {
|
|
name: "nickname".into(),
|
|
attribute_type: AttributeType::String,
|
|
is_list: false,
|
|
is_visible: true,
|
|
is_editable: true,
|
|
is_hardcoded: false,
|
|
is_readonly: false,
|
|
}],
|
|
},
|
|
group_attributes: AttributeList {
|
|
attributes: vec![AttributeSchema {
|
|
name: "club_name".into(),
|
|
attribute_type: AttributeType::String,
|
|
is_list: false,
|
|
is_visible: true,
|
|
is_editable: true,
|
|
is_hardcoded: false,
|
|
is_readonly: false,
|
|
}],
|
|
},
|
|
extra_user_object_classes: vec![
|
|
LdapObjectClass::from("customUserClass"),
|
|
LdapObjectClass::from("myUserClass"),
|
|
],
|
|
extra_group_object_classes: vec![LdapObjectClass::from("customGroupClass")],
|
|
})
|
|
});
|
|
let ldap_handler = setup_bound_readonly_handler(mock).await;
|
|
|
|
let request = make_search_request(
|
|
"dc=example,dc=com",
|
|
LdapFilter::And(vec![]),
|
|
vec!["uid", "nickname", "club_name"],
|
|
);
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Ok(vec![
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
|
|
attributes: vec![
|
|
LdapPartialAttribute {
|
|
atype: "nickname".to_owned(),
|
|
vals: vec![b"Bob the Builder".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "uid".to_owned(),
|
|
vals: vec![b"test".to_vec()],
|
|
},
|
|
],
|
|
}),
|
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
|
dn: "cn=group,ou=groups,dc=example,dc=com".to_owned(),
|
|
attributes: vec![
|
|
LdapPartialAttribute {
|
|
atype: "club_name".to_owned(),
|
|
vals: vec![b"Breakfast Club".to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "uid".to_owned(),
|
|
vals: vec![b"group".to_vec()],
|
|
},
|
|
],
|
|
}),
|
|
make_search_success()
|
|
]),
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_base_scope_non_existent_user() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users().returning(|_, _| Ok(vec![]));
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = LdapSearchRequest {
|
|
scope: LdapSearchScope::Base,
|
|
..make_search_request(
|
|
"uid=nonexistent,ou=people,dc=example,dc=com",
|
|
LdapFilter::And(vec![]),
|
|
vec!["objectClass".to_string()],
|
|
)
|
|
};
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Err(LdapError {
|
|
code: LdapResultCode::NoSuchObject,
|
|
message: "".to_string(),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_base_scope_non_existent_group() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_groups().returning(|_| Ok(vec![]));
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = LdapSearchRequest {
|
|
scope: LdapSearchScope::Base,
|
|
..make_search_request(
|
|
"uid=nonexistent,ou=groups,dc=example,dc=com",
|
|
LdapFilter::And(vec![]),
|
|
vec!["objectClass".to_string()],
|
|
)
|
|
};
|
|
assert_eq!(
|
|
ldap_handler.do_search_or_dse(&request).await,
|
|
Err(LdapError {
|
|
code: LdapResultCode::NoSuchObject,
|
|
message: "".to_string(),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_search_base_scope_existing_user() {
|
|
let mut mock = MockTestBackendHandler::new();
|
|
mock.expect_list_users().returning(|_, _| {
|
|
Ok(vec![UserAndGroups {
|
|
user: User {
|
|
user_id: UserId::new("bob"),
|
|
..Default::default()
|
|
},
|
|
groups: None,
|
|
}])
|
|
});
|
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = LdapSearchRequest {
|
|
scope: LdapSearchScope::Base,
|
|
..make_search_request(
|
|
"uid=bob,ou=people,dc=example,dc=com",
|
|
LdapFilter::And(vec![]),
|
|
vec!["objectClass".to_string()],
|
|
)
|
|
};
|
|
let results = ldap_handler.do_search_or_dse(&request).await.unwrap();
|
|
// Should have 2 results: SearchResultEntry and SearchResultDone
|
|
assert_eq!(results.len(), 2);
|
|
assert!(matches!(results[0], LdapOp::SearchResultEntry(_)));
|
|
assert!(matches!(results[1], LdapOp::SearchResultDone(_)));
|
|
}
|
|
}
|