mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
server: Add support for SubString ldap filter
This commit is contained in:
parent
21e51c3d38
commit
81036943c2
@ -14,13 +14,46 @@ pub struct BindRequest {
|
||||
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)]
|
||||
pub enum UserRequestFilter {
|
||||
And(Vec<UserRequestFilter>),
|
||||
Or(Vec<UserRequestFilter>),
|
||||
Not(Box<UserRequestFilter>),
|
||||
UserId(UserId),
|
||||
UserIdSubString(SubStringFilter),
|
||||
Equality(UserColumn, String),
|
||||
SubString(UserColumn, SubStringFilter),
|
||||
// Check if a user belongs to a group identified by name.
|
||||
MemberOf(String),
|
||||
// Same, by id.
|
||||
@ -43,6 +76,7 @@ pub enum GroupRequestFilter {
|
||||
Or(Vec<GroupRequestFilter>),
|
||||
Not(Box<GroupRequestFilter>),
|
||||
DisplayName(String),
|
||||
DisplayNameSubString(SubStringFilter),
|
||||
Uuid(Uuid),
|
||||
GroupId(GroupId),
|
||||
// Check if the group contains a user identified by uid.
|
||||
|
@ -178,6 +178,21 @@ fn convert_group_filter(
|
||||
|| 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 {
|
||||
code: LdapResultCode::UnwillingToPerform,
|
||||
message: format!("Unsupported group filter: {:?}", filter),
|
||||
|
@ -185,6 +185,28 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult<
|
||||
|| 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 {
|
||||
code: LdapResultCode::UnwillingToPerform,
|
||||
message: format!("Unsupported user filter: {:?}", filter),
|
||||
|
@ -1,12 +1,29 @@
|
||||
use itertools::Itertools;
|
||||
use ldap3_proto::LdapResultCode;
|
||||
use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode};
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use crate::domain::{
|
||||
handler::SubStringFilter,
|
||||
ldap::error::{LdapError, LdapResult},
|
||||
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)>
|
||||
where
|
||||
I: Iterator<Item = String>,
|
||||
@ -141,8 +158,8 @@ pub fn map_user_field(field: &str) -> Option<UserColumn> {
|
||||
"uid" | "user_id" | "id" => UserColumn::UserId,
|
||||
"mail" | "email" => UserColumn::Email,
|
||||
"cn" | "displayname" | "display_name" => UserColumn::DisplayName,
|
||||
"givenname" | "first_name" => UserColumn::FirstName,
|
||||
"sn" | "last_name" => UserColumn::LastName,
|
||||
"givenname" | "first_name" | "firstname" => UserColumn::FirstName,
|
||||
"sn" | "last_name" | "lastname" => UserColumn::LastName,
|
||||
"avatar" | "jpegphoto" => UserColumn::Avatar,
|
||||
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
|
||||
UserColumn::CreationDate
|
||||
|
@ -7,7 +7,7 @@ use crate::domain::{
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{
|
||||
sea_query::{Cond, IntoCondition, SimpleExpr},
|
||||
sea_query::{Alias, Cond, Expr, Func, IntoCondition, SimpleExpr},
|
||||
ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect,
|
||||
QueryTrait,
|
||||
};
|
||||
@ -15,6 +15,7 @@ use tracing::{debug, instrument};
|
||||
|
||||
fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
|
||||
use GroupRequestFilter::*;
|
||||
let group_table = Alias::new("groups");
|
||||
match filter {
|
||||
And(fs) => {
|
||||
if fs.is_empty() {
|
||||
@ -46,6 +47,12 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
|
||||
.into_query(),
|
||||
)
|
||||
.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)]
|
||||
mod tests {
|
||||
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(
|
||||
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]
|
||||
async fn test_get_group_details() {
|
||||
let fixture = TestFixture::new().await;
|
||||
|
@ -8,7 +8,7 @@ use super::{
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{
|
||||
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,
|
||||
QuerySelect, QueryTrait, Set,
|
||||
};
|
||||
@ -49,8 +49,15 @@ fn get_user_filter_expr(filter: UserRequestFilter) -> Cond {
|
||||
MemberOfId(group_id) => Expr::col((group_table, GroupColumn::GroupId))
|
||||
.eq(group_id)
|
||||
.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>> {
|
||||
match opt_name {
|
||||
None => ActiveValue::NotSet,
|
||||
@ -236,6 +243,7 @@ impl UserBackendHandler for SqlBackendHandler {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::{
|
||||
handler::SubStringFilter,
|
||||
sql_backend_handler::tests::*,
|
||||
types::{JpegPhoto, UserColumn},
|
||||
};
|
||||
@ -286,6 +294,31 @@ mod tests {
|
||||
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]
|
||||
async fn test_list_users_false_filter() {
|
||||
let fixture = TestFixture::new().await;
|
||||
|
@ -680,7 +680,7 @@ mod tests {
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use chrono::TimeZone;
|
||||
use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope};
|
||||
use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope, LdapSubstringFilter};
|
||||
use mockall::predicate::eq;
|
||||
use std::collections::HashSet;
|
||||
use tokio;
|
||||
@ -1322,6 +1322,11 @@ mod tests {
|
||||
true.into(),
|
||||
GroupRequestFilter::Not(Box::new(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)
|
||||
.return_once(|_| {
|
||||
@ -1357,6 +1362,14 @@ mod tests {
|
||||
"random_attribUte".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"],
|
||||
);
|
||||
@ -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]
|
||||
async fn test_search_groups_error() {
|
||||
let mut mock = MockTestBackendHandler::new();
|
||||
@ -1478,9 +1507,9 @@ mod tests {
|
||||
async fn test_search_groups_filter_error() {
|
||||
let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
|
||||
let request = make_group_search_request(
|
||||
LdapFilter::And(vec![LdapFilter::Substring(
|
||||
"whatever".to_string(),
|
||||
ldap3_proto::proto::LdapSubstringFilter::default(),
|
||||
LdapFilter::And(vec![LdapFilter::Approx(
|
||||
"whatever".to_owned(),
|
||||
"value".to_owned(),
|
||||
)]),
|
||||
vec!["cn"],
|
||||
);
|
||||
@ -1488,8 +1517,7 @@ mod tests {
|
||||
ldap_handler.do_search_or_dse(&request).await,
|
||||
Err(LdapError {
|
||||
code: LdapResultCode::UnwillingToPerform,
|
||||
message: r#"Unsupported group filter: Substring("whatever", LdapSubstringFilter { initial: None, any: [], final_: None })"#
|
||||
.to_string()
|
||||
message: r#"Unsupported group filter: Approx("whatever", "value")"#.to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -1512,6 +1540,19 @@ mod tests {
|
||||
true.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),
|
||||
@ -1539,6 +1580,22 @@ mod tests {
|
||||
LdapFilter::Present("uid".to_string()),
|
||||
LdapFilter::Present("unknown".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"],
|
||||
);
|
||||
@ -1910,17 +1967,14 @@ mod tests {
|
||||
async fn test_search_unsupported_filters() {
|
||||
let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
|
||||
let request = make_user_search_request(
|
||||
LdapFilter::Substring(
|
||||
"uid".to_string(),
|
||||
ldap3_proto::proto::LdapSubstringFilter::default(),
|
||||
),
|
||||
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: Substring("uid", LdapSubstringFilter { initial: None, any: [], final_: None })"#.to_string()
|
||||
message: r#"Unsupported user filter: Approx("uid", "value")"#.to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user