server: Add support for SubString ldap filter

This commit is contained in:
Valentin Tolmer 2023-02-13 15:41:07 +01:00 committed by nitnelave
parent 21e51c3d38
commit 81036943c2
7 changed files with 219 additions and 19 deletions

View File

@ -14,13 +14,46 @@ pub struct BindRequest {
pub password: String, pub password: String,
} }
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct SubStringFilter {
pub initial: Option<String>,
pub any: Vec<String>,
pub final_: Option<String>,
}
impl SubStringFilter {
pub fn to_sql_filter(&self) -> String {
let mut filter = String::with_capacity(
self.initial.as_ref().map(String::len).unwrap_or_default()
+ 1
+ self.any.iter().map(String::len).sum::<usize>()
+ self.any.len()
+ self.final_.as_ref().map(String::len).unwrap_or_default(),
);
if let Some(f) = &self.initial {
filter.push_str(&f.to_ascii_lowercase());
}
filter.push('%');
for part in self.any.iter() {
filter.push_str(&part.to_ascii_lowercase());
filter.push('%');
}
if let Some(f) = &self.final_ {
filter.push_str(&f.to_ascii_lowercase());
}
filter
}
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub enum UserRequestFilter { pub enum UserRequestFilter {
And(Vec<UserRequestFilter>), And(Vec<UserRequestFilter>),
Or(Vec<UserRequestFilter>), Or(Vec<UserRequestFilter>),
Not(Box<UserRequestFilter>), Not(Box<UserRequestFilter>),
UserId(UserId), UserId(UserId),
UserIdSubString(SubStringFilter),
Equality(UserColumn, String), Equality(UserColumn, String),
SubString(UserColumn, SubStringFilter),
// Check if a user belongs to a group identified by name. // Check if a user belongs to a group identified by name.
MemberOf(String), MemberOf(String),
// Same, by id. // Same, by id.
@ -43,6 +76,7 @@ pub enum GroupRequestFilter {
Or(Vec<GroupRequestFilter>), Or(Vec<GroupRequestFilter>),
Not(Box<GroupRequestFilter>), Not(Box<GroupRequestFilter>),
DisplayName(String), DisplayName(String),
DisplayNameSubString(SubStringFilter),
Uuid(Uuid), Uuid(Uuid),
GroupId(GroupId), GroupId(GroupId),
// Check if the group contains a user identified by uid. // Check if the group contains a user identified by uid.

View File

@ -178,6 +178,21 @@ fn convert_group_filter(
|| map_group_field(field).is_some(), || map_group_field(field).is_some(),
)) ))
} }
LdapFilter::Substring(field, substring_filter) => {
let field = &field.to_ascii_lowercase();
match map_group_field(field.as_str()) {
Some(GroupColumn::DisplayName) => Ok(GroupRequestFilter::DisplayNameSubString(
substring_filter.clone().into(),
)),
_ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!(
"Unsupported group attribute for substring filter: {:?}",
field
),
}),
}
}
_ => Err(LdapError { _ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
message: format!("Unsupported group filter: {:?}", filter), message: format!("Unsupported group filter: {:?}", filter),

View File

@ -185,6 +185,28 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult<
|| map_user_field(field).is_some(), || map_user_field(field).is_some(),
)) ))
} }
LdapFilter::Substring(field, substring_filter) => {
let field = &field.to_ascii_lowercase();
match map_user_field(field.as_str()) {
Some(UserColumn::UserId) => Ok(UserRequestFilter::UserIdSubString(
substring_filter.clone().into(),
)),
None
| Some(UserColumn::CreationDate)
| Some(UserColumn::Avatar)
| Some(UserColumn::Uuid) => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!(
"Unsupported user attribute for substring filter: {:?}",
field
),
}),
Some(field) => Ok(UserRequestFilter::SubString(
field,
substring_filter.clone().into(),
)),
}
}
_ => Err(LdapError { _ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
message: format!("Unsupported user filter: {:?}", filter), message: format!("Unsupported user filter: {:?}", filter),

View File

@ -1,12 +1,29 @@
use itertools::Itertools; use itertools::Itertools;
use ldap3_proto::LdapResultCode; use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode};
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
handler::SubStringFilter,
ldap::error::{LdapError, LdapResult}, ldap::error::{LdapError, LdapResult},
types::{GroupColumn, UserColumn, UserId}, types::{GroupColumn, UserColumn, UserId},
}; };
impl From<LdapSubstringFilter> for SubStringFilter {
fn from(
LdapSubstringFilter {
initial,
any,
final_,
}: LdapSubstringFilter,
) -> Self {
Self {
initial,
any,
final_,
}
}
}
fn make_dn_pair<I>(mut iter: I) -> LdapResult<(String, String)> fn make_dn_pair<I>(mut iter: I) -> LdapResult<(String, String)>
where where
I: Iterator<Item = String>, I: Iterator<Item = String>,
@ -141,8 +158,8 @@ pub fn map_user_field(field: &str) -> Option<UserColumn> {
"uid" | "user_id" | "id" => UserColumn::UserId, "uid" | "user_id" | "id" => UserColumn::UserId,
"mail" | "email" => UserColumn::Email, "mail" | "email" => UserColumn::Email,
"cn" | "displayname" | "display_name" => UserColumn::DisplayName, "cn" | "displayname" | "display_name" => UserColumn::DisplayName,
"givenname" | "first_name" => UserColumn::FirstName, "givenname" | "first_name" | "firstname" => UserColumn::FirstName,
"sn" | "last_name" => UserColumn::LastName, "sn" | "last_name" | "lastname" => UserColumn::LastName,
"avatar" | "jpegphoto" => UserColumn::Avatar, "avatar" | "jpegphoto" => UserColumn::Avatar,
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => { "creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
UserColumn::CreationDate UserColumn::CreationDate

View File

@ -7,7 +7,7 @@ use crate::domain::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use sea_orm::{ use sea_orm::{
sea_query::{Cond, IntoCondition, SimpleExpr}, sea_query::{Alias, Cond, Expr, Func, IntoCondition, SimpleExpr},
ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect,
QueryTrait, QueryTrait,
}; };
@ -15,6 +15,7 @@ use tracing::{debug, instrument};
fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond { fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
use GroupRequestFilter::*; use GroupRequestFilter::*;
let group_table = Alias::new("groups");
match filter { match filter {
And(fs) => { And(fs) => {
if fs.is_empty() { if fs.is_empty() {
@ -46,6 +47,12 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
.into_query(), .into_query(),
) )
.into_condition(), .into_condition(),
DisplayNameSubString(filter) => SimpleExpr::FunctionCall(Func::lower(Expr::col((
group_table,
GroupColumn::DisplayName,
))))
.like(filter.to_sql_filter())
.into_condition(),
} }
} }
@ -146,7 +153,7 @@ impl GroupBackendHandler for SqlBackendHandler {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::domain::{sql_backend_handler::tests::*, types::UserId}; use crate::domain::{handler::SubStringFilter, sql_backend_handler::tests::*, types::UserId};
async fn get_group_ids( async fn get_group_ids(
handler: &SqlBackendHandler, handler: &SqlBackendHandler,
@ -221,6 +228,24 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_list_groups_substring_filter() {
let fixture = TestFixture::new().await;
assert_eq!(
get_group_ids(
&fixture.handler,
Some(GroupRequestFilter::DisplayNameSubString(SubStringFilter {
initial: Some("be".to_owned()),
any: vec!["sT".to_owned()],
final_: Some("P".to_owned()),
})),
)
.await,
// Best group
vec![fixture.groups[0]]
);
}
#[tokio::test] #[tokio::test]
async fn test_get_group_details() { async fn test_get_group_details() {
let fixture = TestFixture::new().await; let fixture = TestFixture::new().await;

View File

@ -8,7 +8,7 @@ use super::{
use async_trait::async_trait; use async_trait::async_trait;
use sea_orm::{ use sea_orm::{
entity::IntoActiveValue, entity::IntoActiveValue,
sea_query::{Alias, Cond, Expr, IntoColumnRef, IntoCondition, SimpleExpr}, sea_query::{Alias, Cond, Expr, Func, IntoColumnRef, IntoCondition, SimpleExpr},
ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder,
QuerySelect, QueryTrait, Set, QuerySelect, QueryTrait, Set,
}; };
@ -49,8 +49,15 @@ fn get_user_filter_expr(filter: UserRequestFilter) -> Cond {
MemberOfId(group_id) => Expr::col((group_table, GroupColumn::GroupId)) MemberOfId(group_id) => Expr::col((group_table, GroupColumn::GroupId))
.eq(group_id) .eq(group_id)
.into_condition(), .into_condition(),
UserIdSubString(filter) => UserColumn::UserId
.like(&filter.to_sql_filter())
.into_condition(),
SubString(col, filter) => SimpleExpr::FunctionCall(Func::lower(Expr::col(col)))
.like(filter.to_sql_filter())
.into_condition(),
} }
} }
fn to_value(opt_name: &Option<String>) -> ActiveValue<Option<String>> { fn to_value(opt_name: &Option<String>) -> ActiveValue<Option<String>> {
match opt_name { match opt_name {
None => ActiveValue::NotSet, None => ActiveValue::NotSet,
@ -236,6 +243,7 @@ impl UserBackendHandler for SqlBackendHandler {
mod tests { mod tests {
use super::*; use super::*;
use crate::domain::{ use crate::domain::{
handler::SubStringFilter,
sql_backend_handler::tests::*, sql_backend_handler::tests::*,
types::{JpegPhoto, UserColumn}, types::{JpegPhoto, UserColumn},
}; };
@ -286,6 +294,31 @@ mod tests {
assert_eq!(users, vec!["bob"]); assert_eq!(users, vec!["bob"]);
} }
#[tokio::test]
async fn test_list_users_substring_filter() {
let fixture = TestFixture::new().await;
let users = get_user_names(
&fixture.handler,
Some(UserRequestFilter::And(vec![
UserRequestFilter::UserIdSubString(SubStringFilter {
initial: Some("Pa".to_owned()),
any: vec!["rI".to_owned()],
final_: Some("K".to_owned()),
}),
UserRequestFilter::SubString(
UserColumn::FirstName,
SubStringFilter {
initial: None,
any: vec!["r".to_owned(), "t".to_owned()],
final_: None,
},
),
])),
)
.await;
assert_eq!(users, vec!["patrick"]);
}
#[tokio::test] #[tokio::test]
async fn test_list_users_false_filter() { async fn test_list_users_false_filter() {
let fixture = TestFixture::new().await; let fixture = TestFixture::new().await;

View File

@ -680,7 +680,7 @@ mod tests {
}; };
use async_trait::async_trait; use async_trait::async_trait;
use chrono::TimeZone; use chrono::TimeZone;
use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope}; use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope, LdapSubstringFilter};
use mockall::predicate::eq; use mockall::predicate::eq;
use std::collections::HashSet; use std::collections::HashSet;
use tokio; use tokio;
@ -1322,6 +1322,11 @@ mod tests {
true.into(), true.into(),
GroupRequestFilter::Not(Box::new(false.into())), GroupRequestFilter::Not(Box::new(false.into())),
false.into(), false.into(),
GroupRequestFilter::DisplayNameSubString(SubStringFilter {
initial: Some("iNIt".to_owned()),
any: vec!["1".to_owned(), "2aA".to_owned()],
final_: Some("finAl".to_owned()),
}),
])))) ]))))
.times(1) .times(1)
.return_once(|_| { .return_once(|_| {
@ -1357,6 +1362,14 @@ mod tests {
"random_attribUte".to_string(), "random_attribUte".to_string(),
))), ))),
LdapFilter::Equality("unknown_attribute".to_string(), "randomValue".to_string()), LdapFilter::Equality("unknown_attribute".to_string(), "randomValue".to_string()),
LdapFilter::Substring(
"cn".to_owned(),
LdapSubstringFilter {
initial: Some("iNIt".to_owned()),
any: vec!["1".to_owned(), "2aA".to_owned()],
final_: Some("finAl".to_owned()),
},
),
]), ]),
vec!["1.1"], vec!["1.1"],
); );
@ -1442,6 +1455,22 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_search_groups_unsupported_substring() {
let mut ldap_handler = setup_bound_admin_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] #[tokio::test]
async fn test_search_groups_error() { async fn test_search_groups_error() {
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();
@ -1478,9 +1507,9 @@ mod tests {
async fn test_search_groups_filter_error() { async fn test_search_groups_filter_error() {
let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
let request = make_group_search_request( let request = make_group_search_request(
LdapFilter::And(vec![LdapFilter::Substring( LdapFilter::And(vec![LdapFilter::Approx(
"whatever".to_string(), "whatever".to_owned(),
ldap3_proto::proto::LdapSubstringFilter::default(), "value".to_owned(),
)]), )]),
vec!["cn"], vec!["cn"],
); );
@ -1488,8 +1517,7 @@ mod tests {
ldap_handler.do_search_or_dse(&request).await, ldap_handler.do_search_or_dse(&request).await,
Err(LdapError { Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
message: r#"Unsupported group filter: Substring("whatever", LdapSubstringFilter { initial: None, any: [], final_: None })"# message: r#"Unsupported group filter: Approx("whatever", "value")"#.to_string()
.to_string()
}) })
); );
} }
@ -1512,6 +1540,19 @@ mod tests {
true.into(), true.into(),
false.into(), false.into(),
false.into(), false.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::FirstName,
SubStringFilter {
initial: Some("iNIt".to_owned()),
any: vec!["1".to_owned(), "2aA".to_owned()],
final_: Some("finAl".to_owned()),
},
),
], ],
)]))), )]))),
eq(false), eq(false),
@ -1539,6 +1580,22 @@ mod tests {
LdapFilter::Present("uid".to_string()), LdapFilter::Present("uid".to_string()),
LdapFilter::Present("unknown".to_string()), LdapFilter::Present("unknown".to_string()),
LdapFilter::Equality("unknown_attribute".to_string(), "randomValue".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(
"firstName".to_owned(),
LdapSubstringFilter {
initial: Some("iNIt".to_owned()),
any: vec!["1".to_owned(), "2aA".to_owned()],
final_: Some("finAl".to_owned()),
},
),
])]), ])]),
vec!["objectClass"], vec!["objectClass"],
); );
@ -1910,17 +1967,14 @@ mod tests {
async fn test_search_unsupported_filters() { async fn test_search_unsupported_filters() {
let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
let request = make_user_search_request( let request = make_user_search_request(
LdapFilter::Substring( LdapFilter::Approx("uid".to_owned(), "value".to_owned()),
"uid".to_string(),
ldap3_proto::proto::LdapSubstringFilter::default(),
),
vec!["objectClass"], vec!["objectClass"],
); );
assert_eq!( assert_eq!(
ldap_handler.do_search_or_dse(&request).await, ldap_handler.do_search_or_dse(&request).await,
Err(LdapError { Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
message: r#"Unsupported user filter: Substring("uid", LdapSubstringFilter { initial: None, any: [], final_: None })"#.to_string() message: r#"Unsupported user filter: Approx("uid", "value")"#.to_string()
}) })
); );
} }