diff --git a/libs/client/src/group.rs b/libs/client/src/group.rs index f5d69389f..eecac4ef9 100644 --- a/libs/client/src/group.rs +++ b/libs/client/src/group.rs @@ -1,6 +1,12 @@ use crate::{ClientError, KanidmClient}; +use kanidm_proto::v1::Entry; impl KanidmClient { + pub async fn idm_group_search(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(&format!("/v1/group/_search/{}", id)) + .await + } + pub async fn idm_group_purge_attr(&self, id: &str, attr: &str) -> Result<(), ClientError> { self.perform_delete_request(format!("/v1/group/{}/_attr/{}", id, attr).as_str()) .await diff --git a/libs/client/src/person.rs b/libs/client/src/person.rs index 62ed6bb4d..a5fdbf71b 100644 --- a/libs/client/src/person.rs +++ b/libs/client/src/person.rs @@ -17,6 +17,11 @@ impl KanidmClient { .await } + pub async fn idm_person_search(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(format!("/v1/person/_search/{}", id).as_str()) + .await + } + pub async fn idm_person_account_create( &self, name: &str, diff --git a/server/core/src/https/apidocs/mod.rs b/server/core/src/https/apidocs/mod.rs index b635086e4..ea8abdd69 100644 --- a/server/core/src/https/apidocs/mod.rs +++ b/server/core/src/https/apidocs/mod.rs @@ -90,6 +90,7 @@ impl Modify for SecurityAddon { super::v1::service_account_api_token_delete, super::v1::service_account_api_token_get, super::v1::service_account_api_token_post, + super::v1::person_search_id, super::v1::person_id_get, super::v1::person_id_patch, super::v1::person_id_delete, @@ -166,6 +167,7 @@ impl Modify for SecurityAddon { super::v1::group_id_unix_post, super::v1::group_get, super::v1::group_post, + super::v1::group_search_id, super::v1::group_id_get, super::v1::group_id_patch, super::v1::group_id_delete, diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index 3c3533314..3d3145650 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -629,6 +629,30 @@ pub async fn person_post( json_rest_event_post(state, classes, obj, kopid, client_auth_info).await } +#[utoipa::path( + get, + path = "/v1/person/_search/{id}", + responses( + (status=200, body=Option, content_type="application/json"), + ApiResponseWithout200, + ), + security(("token_jwt" = [])), + tag = "v1/person", + operation_id = "person_search_id", +)] +pub async fn person_search_id( + State(state): State, + Path(id): Path, + Extension(kopid): Extension, + VerifiedClientInformation(client_auth_info): VerifiedClientInformation, +) -> Result>, WebError> { + let filter = filter_all!(f_and!([ + f_eq(Attribute::Class, EntryClass::Person.into()), + f_sub(Attribute::Name, PartialValue::new_iname(&id)) + ])); + json_rest_event_get(state, None, filter, kopid, client_auth_info).await +} + #[utoipa::path( get, path = "/v1/person/{id}", @@ -2184,6 +2208,30 @@ pub async fn group_get( json_rest_event_get(state, None, filter, kopid, client_auth_info).await } +#[utoipa::path( + get, + path = "/v1/group/_search/{id}", + responses( + (status=200, body=Option, content_type="application/json"), + ApiResponseWithout200, + ), + security(("token_jwt" = [])), + tag = "v1/group", + operation_id = "group_search_id", +)] +pub async fn group_search_id( + State(state): State, + Extension(kopid): Extension, + VerifiedClientInformation(client_auth_info): VerifiedClientInformation, + Path(id): Path, +) -> Result>, WebError> { + let filter = filter_all!(f_and!([ + f_eq(Attribute::Class, EntryClass::Group.into()), + f_sub(Attribute::Name, PartialValue::new_iname(&id)) + ])); + json_rest_event_get(state, None, filter, kopid, client_auth_info).await +} + #[utoipa::path( post, path = "/v1/group", @@ -3145,6 +3193,7 @@ pub(crate) fn route_setup(state: ServerState) -> Router { .route("/v1/self/_applinks", get(applinks_get)) // Person routes .route("/v1/person", get(person_get).post(person_post)) + .route("/v1/person/_search/:id", get(person_search_id)) .route( "/v1/person/:id", get(person_id_get) @@ -3311,6 +3360,7 @@ pub(crate) fn route_setup(state: ServerState) -> Router { .route("/v1/group/:id/_unix/_token", get(group_id_unix_token_get)) .route("/v1/group/:id/_unix", post(group_id_unix_post)) .route("/v1/group", get(group_get).post(group_post)) + .route("/v1/group/_search/:id", get(group_search_id)) .route( "/v1/group/:id", get(group_id_get) diff --git a/server/lib/src/be/mod.rs b/server/lib/src/be/mod.rs index d6d70b6ea..bfcb556b7 100644 --- a/server/lib/src/be/mod.rs +++ b/server/lib/src/be/mod.rs @@ -30,6 +30,7 @@ use crate::repl::ruv::{ ReplicationUpdateVector, ReplicationUpdateVectorReadTransaction, ReplicationUpdateVectorTransaction, ReplicationUpdateVectorWriteTransaction, }; +use crate::utils::trigraph_iter; use crate::value::{IndexType, Value}; pub(crate) mod dbentry; @@ -51,6 +52,7 @@ use kanidm_proto::internal::FsType; // Currently disabled due to improvements in idlset for intersection handling. const FILTER_SEARCH_TEST_THRESHOLD: usize = 0; const FILTER_EXISTS_TEST_THRESHOLD: usize = 0; +const FILTER_SUBSTR_TEST_THRESHOLD: usize = 4; #[derive(Debug, Clone)] /// Limits on the resources a single event can consume. These are defined per-event @@ -216,7 +218,6 @@ pub trait BackendTransaction { /// Recursively apply a filter, transforming into IdList's on the way. This builds a query /// execution log, so that it can be examined how an operation proceeded. #[allow(clippy::cognitive_complexity)] - // #[instrument(level = "debug", name = "be::filter2idl", skip_all)] fn filter2idl( &mut self, filt: &FilterResolved, @@ -243,34 +244,18 @@ pub trait BackendTransaction { (IdList::AllIds, FilterPlan::EqUnindexed(attr.clone())) } } - FilterResolved::Cnt(attr, subvalue, idx) => { - if idx.is_some() { - // Get the idx_key - let idx_key = subvalue.get_idx_sub_key(); - // Get the idl for this - match self - .get_idlayer() - .get_idl(attr, IndexType::SubString, &idx_key)? - { - Some(idl) => ( - IdList::Indexed(idl), - FilterPlan::SubIndexed(attr.clone(), idx_key), - ), - None => (IdList::AllIds, FilterPlan::SubCorrupt(attr.clone())), - } + FilterResolved::Stw(attr, subvalue, idx) + | FilterResolved::Enw(attr, subvalue, idx) + | FilterResolved::Cnt(attr, subvalue, idx) => { + // Get the idx_key. Not all types support this, so may return "none". + trace!(?idx, ?subvalue, ?attr); + if let (true, Some(idx_key)) = (idx.is_some(), subvalue.get_idx_sub_key()) { + self.filter2idl_sub(attr, idx_key)? } else { // Schema believes this is not indexed (IdList::AllIds, FilterPlan::SubUnindexed(attr.clone())) } } - FilterResolved::Stw(attr, _subvalue, _idx) => { - // Schema believes this is not indexed - (IdList::AllIds, FilterPlan::SubUnindexed(attr.clone())) - } - FilterResolved::Enw(attr, _subvalue, _idx) => { - // Schema believes this is not indexed - (IdList::AllIds, FilterPlan::SubUnindexed(attr.clone())) - } FilterResolved::Pres(attr, idx) => { if idx.is_some() { // Get the idl for this @@ -579,6 +564,69 @@ pub trait BackendTransaction { }) } + fn filter2idl_sub( + &mut self, + attr: &AttrString, + sub_idx_key: String, + ) -> Result<(IdList, FilterPlan), OperationError> { + // Now given that idx_key, we will iterate over the possible graphemes. + let mut grapheme_iter = trigraph_iter(&sub_idx_key); + + // Substrings are always partial because we have to split the keys up + // and we don't pay attention to starts/ends with conditions. We need + // the caller to check those conditions manually at run time. This lets + // the index focus on trigraph indexes only rather than needing to + // worry about those other bits. In a way substring indexes are "fuzzy". + + let mut idl = match grapheme_iter.next() { + Some(idx_key) => { + match self + .get_idlayer() + .get_idl(attr, IndexType::SubString, &idx_key)? + { + Some(idl) => idl, + None => return Ok((IdList::AllIds, FilterPlan::SubCorrupt(attr.clone()))), + } + } + None => { + // If there are no graphemes this means the attempt is for an empty string, so + // we return an empty result set. + return Ok((IdList::Indexed(IDLBitRange::new()), FilterPlan::Invalid)); + } + }; + + if idl.len() > FILTER_SUBSTR_TEST_THRESHOLD { + for idx_key in grapheme_iter { + // Get the idl for this + match self + .get_idlayer() + .get_idl(attr, IndexType::SubString, idx_key)? + { + Some(r_idl) => { + // Do an *and* operation between what we found and our working idl. + idl = r_idl & idl; + } + None => { + // if something didn't match, then we simply bail out after zeroing the current IDL. + idl = IDLBitRange::new(); + } + }; + + if idl.len() < FILTER_SUBSTR_TEST_THRESHOLD { + break; + } + } + } else { + drop(grapheme_iter); + } + + // We exhausted the grapheme iter, exit with what we found. + Ok(( + IdList::Partial(idl), + FilterPlan::SubIndexed(attr.clone(), sub_idx_key), + )) + } + #[instrument(level = "debug", name = "be::search", skip_all)] fn search( &mut self, @@ -2723,6 +2771,40 @@ mod tests { Some(vec![2]) ); + for sub in [ + "w", "m", "wi", "il", "ll", "li", "ia", "am", "wil", "ill", "lli", "lia", "iam", + ] { + idl_state!( + be, + Attribute::Name.as_ref(), + IndexType::SubString, + sub, + Some(vec![1]) + ); + } + + for sub in [ + "c", "r", "e", "cl", "la", "ai", "ir", "re", "cla", "lai", "air", "ire", + ] { + idl_state!( + be, + Attribute::Name.as_ref(), + IndexType::SubString, + sub, + Some(vec![2]) + ); + } + + for sub in ["i", "a", "l"] { + idl_state!( + be, + Attribute::Name.as_ref(), + IndexType::SubString, + sub, + Some(vec![1, 2]) + ); + } + idl_state!( be, Attribute::Name.as_ref(), @@ -3239,9 +3321,7 @@ mod tests { IdList::Partial(idl) => { assert!(idl == IDLBitRange::from_iter(vec![1])); } - _ => { - panic!(""); - } + _ => unreachable!(), } let (r, _plan) = be.filter2idl(f_p2.to_inner(), 0).unwrap(); @@ -3249,9 +3329,19 @@ mod tests { IdList::Partial(idl) => { assert!(idl == IDLBitRange::from_iter(vec![1])); } - _ => { - panic!(""); + _ => unreachable!(), + } + + // Substrings are always partial + let f_p3 = filter_resolved!(f_sub(Attribute::Name, PartialValue::new_utf8s("wil"))); + + let (r, plan) = be.filter2idl(f_p3.to_inner(), 0).unwrap(); + trace!(?r, ?plan); + match r { + IdList::Partial(idl) => { + assert!(idl == IDLBitRange::from_iter(vec![1])); } + _ => unreachable!(), } // no index and diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs index 20df8a28b..032535446 100644 --- a/server/lib/src/constants/schema.rs +++ b/server/lib/src/constants/schema.rs @@ -21,6 +21,17 @@ pub static ref SCHEMA_ATTR_DISPLAYNAME: SchemaAttribute = SchemaAttribute { ..Default::default() }; +pub static ref SCHEMA_ATTR_DISPLAYNAME_DL7: SchemaAttribute = SchemaAttribute { + uuid: UUID_SCHEMA_ATTR_DISPLAYNAME, + name: Attribute::DisplayName.into(), + description: "The publicly visible display name of this person".to_string(), + + index: vec![IndexType::Equality, IndexType::SubString], + sync_allowed: true, + syntax: SyntaxType::Utf8String, + ..Default::default() +}; + pub static ref SCHEMA_ATTR_MAIL: SchemaAttribute = SchemaAttribute { uuid: UUID_SCHEMA_ATTR_MAIL, name: Attribute::Mail.into(), @@ -34,6 +45,19 @@ pub static ref SCHEMA_ATTR_MAIL: SchemaAttribute = SchemaAttribute { ..Default::default() }; +pub static ref SCHEMA_ATTR_MAIL_DL7: SchemaAttribute = SchemaAttribute { + uuid: UUID_SCHEMA_ATTR_MAIL, + name: Attribute::Mail.into(), + description: "Mail addresses of the object".to_string(), + + index: vec![IndexType::Equality, IndexType::SubString], + unique: true, + multivalue: true, + sync_allowed: true, + syntax: SyntaxType::EmailAddress, + ..Default::default() +}; + pub static ref SCHEMA_ATTR_EC_KEY_PRIVATE: SchemaAttribute = SchemaAttribute { uuid: UUID_SCHEMA_ATTR_EC_KEY_PRIVATE, name: Attribute::IdVerificationEcKey.into(), @@ -79,6 +103,17 @@ pub static ref SCHEMA_ATTR_LEGALNAME: SchemaAttribute = SchemaAttribute { ..Default::default() }; +pub static ref SCHEMA_ATTR_LEGALNAME_DL7: SchemaAttribute = SchemaAttribute { + uuid: UUID_SCHEMA_ATTR_LEGALNAME, + name: Attribute::LegalName.into(), + description: "The private and sensitive legal name of this person".to_string(), + + index: vec![IndexType::Equality, IndexType::SubString], + sync_allowed: true, + syntax: SyntaxType::Utf8String, + ..Default::default() +}; + pub static ref SCHEMA_ATTR_NAME_HISTORY: SchemaAttribute = SchemaAttribute { uuid: UUID_SCHEMA_ATTR_NAME_HISTORY, name: Attribute::NameHistory.into(), diff --git a/server/lib/src/entry.rs b/server/lib/src/entry.rs index d18b28a3c..e0ce66317 100644 --- a/server/lib/src/entry.rs +++ b/server/lib/src/entry.rs @@ -1646,7 +1646,11 @@ impl Entry { IndexType::Presence => { vec![Err((&ikey.attr, ikey.itype, "_".to_string()))] } - IndexType::SubString => Vec::with_capacity(0), + IndexType::SubString => vs + .generate_idx_sub_keys() + .into_iter() + .map(|idx_key| Err((&ikey.attr, ikey.itype, idx_key))) + .collect(), }; changes } @@ -1682,7 +1686,11 @@ impl Entry { IndexType::Presence => { vec![Ok((&ikey.attr, ikey.itype, "_".to_string()))] } - IndexType::SubString => Vec::with_capacity(0), + IndexType::SubString => vs + .generate_idx_sub_keys() + .into_iter() + .map(|idx_key| Ok((&ikey.attr, ikey.itype, idx_key))) + .collect(), }; // For each value // @@ -1728,7 +1736,11 @@ impl Entry { IndexType::Presence => { vec![Err((&ikey.attr, ikey.itype, "_".to_string()))] } - IndexType::SubString => Vec::with_capacity(0), + IndexType::SubString => pre_vs + .generate_idx_sub_keys() + .into_iter() + .map(|idx_key| Err((&ikey.attr, ikey.itype, idx_key))) + .collect(), }; changes } @@ -1747,17 +1759,30 @@ impl Entry { IndexType::Presence => { vec![Ok((&ikey.attr, ikey.itype, "_".to_string()))] } - IndexType::SubString => Vec::with_capacity(0), + IndexType::SubString => post_vs + .generate_idx_sub_keys() + .into_iter() + .map(|idx_key| Ok((&ikey.attr, ikey.itype, idx_key))) + .collect(), }; changes } (Some(pre_vs), Some(post_vs)) => { // it exists in both, we need to work out the difference within the attr. - - let mut pre_idx_keys = pre_vs.generate_idx_eq_keys(); - pre_idx_keys.sort_unstable(); - let mut post_idx_keys = post_vs.generate_idx_eq_keys(); - post_idx_keys.sort_unstable(); + let (mut pre_idx_keys, mut post_idx_keys) = match ikey.itype { + IndexType::Equality => ( + pre_vs.generate_idx_eq_keys(), + post_vs.generate_idx_eq_keys(), + ), + IndexType::Presence => { + // No action - we still are "present", so nothing to do! + (Vec::with_capacity(0), Vec::with_capacity(0)) + } + IndexType::SubString => ( + pre_vs.generate_idx_sub_keys(), + post_vs.generate_idx_sub_keys(), + ), + }; let sz = if pre_idx_keys.len() > post_idx_keys.len() { pre_idx_keys.len() @@ -1765,54 +1790,58 @@ impl Entry { post_idx_keys.len() }; - let mut pre_iter = pre_idx_keys.iter(); - let mut post_iter = post_idx_keys.iter(); - - let mut pre = pre_iter.next(); - let mut post = post_iter.next(); - let mut added_vs = Vec::with_capacity(sz); - let mut removed_vs = Vec::with_capacity(sz); - loop { - match (pre, post) { - (Some(a), Some(b)) => { - match a.cmp(b) { - Ordering::Less => { - removed_vs.push(a.clone()); - pre = pre_iter.next(); - } - Ordering::Equal => { - // In both - no action needed. - pre = pre_iter.next(); - post = post_iter.next(); - } - Ordering::Greater => { - added_vs.push(b.clone()); - post = post_iter.next(); + if sz > 0 { + pre_idx_keys.sort_unstable(); + post_idx_keys.sort_unstable(); + + let mut pre_iter = pre_idx_keys.iter(); + let mut post_iter = post_idx_keys.iter(); + + let mut pre = pre_iter.next(); + let mut post = post_iter.next(); + + loop { + match (pre, post) { + (Some(a), Some(b)) => { + match a.cmp(b) { + Ordering::Less => { + removed_vs.push(a.clone()); + pre = pre_iter.next(); + } + Ordering::Equal => { + // In both - no action needed. + pre = pre_iter.next(); + post = post_iter.next(); + } + Ordering::Greater => { + added_vs.push(b.clone()); + post = post_iter.next(); + } } } - } - (Some(a), None) => { - removed_vs.push(a.clone()); - pre = pre_iter.next(); - } - (None, Some(b)) => { - added_vs.push(b.clone()); - post = post_iter.next(); - } - (None, None) => { - break; + (Some(a), None) => { + removed_vs.push(a.clone()); + pre = pre_iter.next(); + } + (None, Some(b)) => { + added_vs.push(b.clone()); + post = post_iter.next(); + } + (None, None) => { + break; + } } } - } + } // end sz > 0 let mut diff = Vec::with_capacity(removed_vs.len() + added_vs.len()); match ikey.itype { - IndexType::Equality => { + IndexType::SubString | IndexType::Equality => { removed_vs .into_iter() .map(|idx_key| Err((&ikey.attr, ikey.itype, idx_key))) @@ -1825,7 +1854,6 @@ impl Entry { IndexType::Presence => { // No action - we still are "present", so nothing to do! } - IndexType::SubString => {} }; // Return the diff diff diff --git a/server/lib/src/filter.rs b/server/lib/src/filter.rs index 6c8390dfc..76a4b6578 100644 --- a/server/lib/src/filter.rs +++ b/server/lib/src/filter.rs @@ -1235,13 +1235,20 @@ impl FilterResolved { } FilterComp::SelfUuid => panic!("Not possible to resolve SelfUuid in from_invalid!"), FilterComp::Cnt(a, v) => { - // TODO: For now, don't emit substring indexes. - // let idx = idxmeta.contains(&(&a, &IndexType::SubString)); - // let idx = NonZeroU8::new(idx as u8); - FilterResolved::Cnt(a, v, None) + let idx = idxmeta.contains(&(&a, &IndexType::SubString)); + let idx = NonZeroU8::new(idx as u8); + FilterResolved::Cnt(a, v, idx) + } + FilterComp::Stw(a, v) => { + let idx = idxmeta.contains(&(&a, &IndexType::SubString)); + let idx = NonZeroU8::new(idx as u8); + FilterResolved::Stw(a, v, idx) + } + FilterComp::Enw(a, v) => { + let idx = idxmeta.contains(&(&a, &IndexType::SubString)); + let idx = NonZeroU8::new(idx as u8); + FilterResolved::Enw(a, v, idx) } - FilterComp::Stw(a, v) => FilterResolved::Stw(a, v, None), - FilterComp::Enw(a, v) => FilterResolved::Enw(a, v, None), FilterComp::Pres(a) => { let idx = idxmeta.contains(&(&a, &IndexType::Presence)); FilterResolved::Pres(a, NonZeroU8::new(idx as u8)) @@ -1335,26 +1342,20 @@ impl FilterResolved { Some(FilterResolved::Cnt(a, v, idx)) } FilterComp::Stw(a, v) => { - /* let idxkref = IdxKeyRef::new(&a, &IndexType::SubString); let idx = idxmeta .get(&idxkref as &dyn IdxKeyToRef) .copied() .and_then(NonZeroU8::new); Some(FilterResolved::Stw(a, v, idx)) - */ - Some(FilterResolved::Stw(a, v, None)) } FilterComp::Enw(a, v) => { - /* let idxkref = IdxKeyRef::new(&a, &IndexType::SubString); let idx = idxmeta .get(&idxkref as &dyn IdxKeyToRef) .copied() .and_then(NonZeroU8::new); Some(FilterResolved::Enw(a, v, idx)) - */ - Some(FilterResolved::Enw(a, v, None)) } FilterComp::Pres(a) => { let idxkref = IdxKeyRef::new(&a, &IndexType::Presence); diff --git a/server/lib/src/schema.rs b/server/lib/src/schema.rs index 60fc4c29d..4b1fc2885 100644 --- a/server/lib/src/schema.rs +++ b/server/lib/src/schema.rs @@ -921,7 +921,11 @@ impl<'a> SchemaWriteTransaction<'a> { phantom: false, sync_allowed: true, replicated: true, - index: vec![IndexType::Equality, IndexType::Presence], + index: vec![ + IndexType::Equality, + IndexType::Presence, + IndexType::SubString, + ], syntax: SyntaxType::Utf8StringIname, }, ); @@ -1254,7 +1258,7 @@ impl<'a> SchemaWriteTransaction<'a> { phantom: false, sync_allowed: false, replicated: true, - index: vec![IndexType::Equality, IndexType::SubString], + index: vec![IndexType::Equality], syntax: SyntaxType::JsonFilter, }, ); @@ -1289,7 +1293,7 @@ impl<'a> SchemaWriteTransaction<'a> { phantom: false, sync_allowed: false, replicated: true, - index: vec![IndexType::Equality, IndexType::SubString], + index: vec![IndexType::Equality], syntax: SyntaxType::JsonFilter, }, ); diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index 7170d2e5a..396e57e6d 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -718,6 +718,9 @@ impl<'a> QueryServerWriteTransaction<'a> { SCHEMA_ATTR_CERTIFICATE_DL7.clone().into(), SCHEMA_ATTR_OAUTH2_RS_ORIGIN_DL7.clone().into(), SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI_DL7.clone().into(), + SCHEMA_ATTR_MAIL_DL7.clone().into(), + SCHEMA_ATTR_LEGALNAME_DL7.clone().into(), + SCHEMA_ATTR_DISPLAYNAME_DL7.clone().into(), SCHEMA_CLASS_DOMAIN_INFO_DL7.clone().into(), SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(), SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(), diff --git a/server/lib/src/utils.rs b/server/lib/src/utils.rs index e843f758f..521e5e800 100644 --- a/server/lib/src/utils.rs +++ b/server/lib/src/utils.rs @@ -1,7 +1,10 @@ +//! `utils.rs` - the projects kitchen junk drawer. + use crate::prelude::*; use hashbrown::HashSet; use rand::distributions::{Distribution, Uniform}; use rand::{thread_rng, Rng}; +use std::ops::Range; #[derive(Debug)] pub struct DistinctAlpha; @@ -85,12 +88,69 @@ impl Distribution for DistinctAlpha { } } +pub(crate) struct GraphemeClusterIter<'a> { + value: &'a str, + char_bounds: Vec, + window: usize, + range: Range, +} + +impl<'a> GraphemeClusterIter<'a> { + pub fn new(value: &'a str, window: usize) -> Self { + let char_bounds = if value.len() < window { + Vec::with_capacity(0) + } else { + let mut char_bounds = Vec::with_capacity(value.len()); + for idx in 0..value.len() { + if value.is_char_boundary(idx) { + char_bounds.push(idx); + } + } + char_bounds.push(value.len()); + char_bounds + }; + + let window_max = char_bounds.len().checked_sub(window).unwrap_or(0); + let range = 0..window_max; + + GraphemeClusterIter { + value, + char_bounds, + window, + range, + } + } +} + +impl<'a> Iterator for GraphemeClusterIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option<&'a str> { + self.range.next().map(|idx| { + let min = self.char_bounds[idx]; + let max = self.char_bounds[idx + self.window]; + &self.value[min..max] + }) + } + + fn size_hint(&self) -> (usize, Option) { + let clusters = self.char_bounds.len().checked_sub(1).unwrap_or(0); + (clusters, Some(clusters)) + } +} + +pub(crate) fn trigraph_iter(value: &str) -> impl Iterator { + GraphemeClusterIter::new(value, 3) + .chain(GraphemeClusterIter::new(value, 2)) + .chain(GraphemeClusterIter::new(value, 1)) +} + #[cfg(test)] mod tests { use crate::prelude::*; use std::time::Duration; - use crate::utils::{uuid_from_duration, uuid_to_gid_u32}; + use crate::utils::{uuid_from_duration, uuid_to_gid_u32, GraphemeClusterIter}; #[test] fn test_utils_uuid_from_duration() { @@ -121,4 +181,35 @@ mod tests { let r3 = uuid_to_gid_u32(u3); assert!(r3 == 0x12345678); } + + #[test] + fn test_utils_grapheme_cluster_iter() { + let d = "โค๏ธ๐Ÿงก๐Ÿ’›๐Ÿ’š๐Ÿ’™๐Ÿ’œ"; + + let gc_expect = vec!["โค", "\u{fe0f}", "๐Ÿงก", "๐Ÿ’›", "๐Ÿ’š", "๐Ÿ’™", "๐Ÿ’œ"]; + let gc: Vec<_> = GraphemeClusterIter::new(d, 1).collect(); + assert_eq!(gc, gc_expect); + + let gc_expect = vec!["โค\u{fe0f}", "\u{fe0f}๐Ÿงก", "๐Ÿงก๐Ÿ’›", "๐Ÿ’›๐Ÿ’š", "๐Ÿ’š๐Ÿ’™", "๐Ÿ’™๐Ÿ’œ"]; + let gc: Vec<_> = GraphemeClusterIter::new(d, 2).collect(); + assert_eq!(gc, gc_expect); + + let gc_expect = vec!["โค\u{fe0f}๐Ÿงก", "\u{fe0f}๐Ÿงก๐Ÿ’›", "๐Ÿงก๐Ÿ’›๐Ÿ’š", "๐Ÿ’›๐Ÿ’š๐Ÿ’™", "๐Ÿ’š๐Ÿ’™๐Ÿ’œ"]; + let gc: Vec<_> = GraphemeClusterIter::new(d, 3).collect(); + assert_eq!(gc, gc_expect); + + let d = "๐Ÿคท๐Ÿฟโ€โ™‚๏ธ"; + + let gc_expect = vec!["๐Ÿคท", "๐Ÿฟ", "\u{200d}", "โ™‚", "\u{fe0f}"]; + let gc: Vec<_> = GraphemeClusterIter::new(d, 1).collect(); + assert_eq!(gc, gc_expect); + + let gc_expect = vec!["๐Ÿคท๐Ÿฟ", "๐Ÿฟ\u{200d}", "\u{200d}โ™‚", "โ™‚\u{fe0f}"]; + let gc: Vec<_> = GraphemeClusterIter::new(d, 2).collect(); + assert_eq!(gc, gc_expect); + + let gc_expect = vec!["๐Ÿคท๐Ÿฟ\u{200d}", "๐Ÿฟ\u{200d}โ™‚", "\u{200d}โ™‚\u{fe0f}"]; + let gc: Vec<_> = GraphemeClusterIter::new(d, 3).collect(); + assert_eq!(gc, gc_expect); + } } diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs index 4b4ffb1f9..8201855ad 100644 --- a/server/lib/src/value.rs +++ b/server/lib/src/value.rs @@ -890,9 +890,22 @@ impl PartialValue { } } - #[allow(clippy::unimplemented)] - pub fn get_idx_sub_key(&self) -> String { - unimplemented!(); + pub fn get_idx_sub_key(&self) -> Option { + match self { + PartialValue::Utf8(s) + | PartialValue::Iutf8(s) + | PartialValue::Iname(s) + // | PartialValue::Nsuniqueid(s) + | PartialValue::EmailAddress(s) + | PartialValue::RestrictedString(s) => Some(s.to_lowercase()), + + PartialValue::Cred(tag) + | PartialValue::PublicBinary(tag) + | PartialValue::SshKey(tag) => Some(tag.to_lowercase()), + + // PartialValue::Spn(name, realm) => format!("{name}@{realm}"), + _ => None, + } } } diff --git a/server/lib/src/valueset/address.rs b/server/lib/src/valueset/address.rs index 71ea74ee1..7991abeaa 100644 --- a/server/lib/src/valueset/address.rs +++ b/server/lib/src/valueset/address.rs @@ -6,6 +6,7 @@ use crate::be::dbvalue::DbValueAddressV1; use crate::prelude::*; use crate::repl::proto::{ReplAddressV1, ReplAttrV1}; use crate::schema::SchemaAttribute; +use crate::utils::trigraph_iter; use crate::value::{Address, VALIDATE_EMAIL_RE}; use crate::valueset::{DbValueSetV2, ValueSet}; @@ -380,6 +381,16 @@ impl ValueSetT for ValueSetEmailAddress { self.set.iter().cloned().collect() } + fn generate_idx_sub_keys(&self) -> Vec { + let lower: Vec<_> = self.set.iter().map(|s| s.to_lowercase()).collect(); + let mut trigraphs: Vec<_> = lower.iter().flat_map(|v| trigraph_iter(v)).collect(); + + trigraphs.sort_unstable(); + trigraphs.dedup(); + + trigraphs.into_iter().map(String::from).collect() + } + fn syntax(&self) -> SyntaxType { SyntaxType::EmailAddress } diff --git a/server/lib/src/valueset/binary.rs b/server/lib/src/valueset/binary.rs index a81fd9c4e..41bde623f 100644 --- a/server/lib/src/valueset/binary.rs +++ b/server/lib/src/valueset/binary.rs @@ -7,6 +7,7 @@ use smolset::SmolSet; use crate::prelude::*; use crate::repl::proto::ReplAttrV1; use crate::schema::SchemaAttribute; +use crate::utils::trigraph_iter; use crate::valueset::{DbValueSetV2, ValueSet}; #[derive(Debug, Clone)] @@ -255,6 +256,16 @@ impl ValueSetT for ValueSetPublicBinary { self.map.keys().cloned().collect() } + fn generate_idx_sub_keys(&self) -> Vec { + let lower: Vec<_> = self.map.keys().map(|s| s.to_lowercase()).collect(); + let mut trigraphs: Vec<_> = lower.iter().flat_map(|v| trigraph_iter(v)).collect(); + + trigraphs.sort_unstable(); + trigraphs.dedup(); + + trigraphs.into_iter().map(String::from).collect() + } + fn syntax(&self) -> SyntaxType { unreachable!(); // SyntaxType::PublicBinary diff --git a/server/lib/src/valueset/cred.rs b/server/lib/src/valueset/cred.rs index 2de252426..604395a40 100644 --- a/server/lib/src/valueset/cred.rs +++ b/server/lib/src/valueset/cred.rs @@ -15,6 +15,7 @@ use crate::repl::proto::{ ReplAttestedPasskeyV4V1, ReplAttrV1, ReplCredV1, ReplIntentTokenV1, ReplPasskeyV4V1, }; use crate::schema::SchemaAttribute; +use crate::utils::trigraph_iter; use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState}; use crate::valueset::{DbValueSetV2, ValueSet}; @@ -124,6 +125,16 @@ impl ValueSetT for ValueSetCredential { self.map.keys().cloned().collect() } + fn generate_idx_sub_keys(&self) -> Vec { + let lower: Vec<_> = self.map.keys().map(|s| s.to_lowercase()).collect(); + let mut trigraphs: Vec<_> = lower.iter().flat_map(|v| trigraph_iter(v)).collect(); + + trigraphs.sort_unstable(); + trigraphs.dedup(); + + trigraphs.into_iter().map(String::from).collect() + } + fn syntax(&self) -> SyntaxType { SyntaxType::Credential } diff --git a/server/lib/src/valueset/iname.rs b/server/lib/src/valueset/iname.rs index ece2f5ea1..e540dd585 100644 --- a/server/lib/src/valueset/iname.rs +++ b/server/lib/src/valueset/iname.rs @@ -3,6 +3,7 @@ use std::collections::BTreeSet; use crate::prelude::*; use crate::repl::proto::ReplAttrV1; use crate::schema::SchemaAttribute; +use crate::utils::trigraph_iter; use crate::valueset::{DbValueSetV2, ValueSet}; #[derive(Debug, Clone)] @@ -117,6 +118,16 @@ impl ValueSetT for ValueSetIname { self.set.iter().cloned().collect() } + fn generate_idx_sub_keys(&self) -> Vec { + let lower: Vec<_> = self.set.iter().map(|s| s.to_lowercase()).collect(); + let mut trigraphs: Vec<_> = lower.iter().flat_map(|v| trigraph_iter(v)).collect(); + + trigraphs.sort_unstable(); + trigraphs.dedup(); + + trigraphs.into_iter().map(String::from).collect() + } + fn syntax(&self) -> SyntaxType { SyntaxType::Utf8StringIname } diff --git a/server/lib/src/valueset/iutf8.rs b/server/lib/src/valueset/iutf8.rs index 38572425f..971006953 100644 --- a/server/lib/src/valueset/iutf8.rs +++ b/server/lib/src/valueset/iutf8.rs @@ -4,6 +4,7 @@ use super::iname::ValueSetIname; use crate::prelude::*; use crate::repl::proto::ReplAttrV1; use crate::schema::SchemaAttribute; +use crate::utils::trigraph_iter; use crate::valueset::{DbValueSetV2, ValueSet}; #[derive(Debug, Clone)] @@ -118,6 +119,15 @@ impl ValueSetT for ValueSetIutf8 { self.set.iter().cloned().collect() } + fn generate_idx_sub_keys(&self) -> Vec { + let mut trigraphs: Vec<_> = self.set.iter().flat_map(|v| trigraph_iter(v)).collect(); + + trigraphs.sort_unstable(); + trigraphs.dedup(); + + trigraphs.into_iter().map(String::from).collect() + } + fn syntax(&self) -> SyntaxType { SyntaxType::Utf8StringInsensitive } diff --git a/server/lib/src/valueset/mod.rs b/server/lib/src/valueset/mod.rs index dad78230a..d53b7abff 100644 --- a/server/lib/src/valueset/mod.rs +++ b/server/lib/src/valueset/mod.rs @@ -132,6 +132,10 @@ pub trait ValueSetT: std::fmt::Debug + DynClone { fn generate_idx_eq_keys(&self) -> Vec; + fn generate_idx_sub_keys(&self) -> Vec { + Vec::with_capacity(0) + } + fn syntax(&self) -> SyntaxType; fn validate(&self, schema_attr: &SchemaAttribute) -> bool; diff --git a/server/lib/src/valueset/restricted.rs b/server/lib/src/valueset/restricted.rs index 0a201292a..a2860a3e7 100644 --- a/server/lib/src/valueset/restricted.rs +++ b/server/lib/src/valueset/restricted.rs @@ -3,6 +3,7 @@ use std::collections::BTreeSet; use crate::prelude::*; use crate::repl::proto::ReplAttrV1; use crate::schema::SchemaAttribute; +use crate::utils::trigraph_iter; use crate::valueset::{DbValueSetV2, ValueSet}; #[derive(Debug, Clone)] @@ -117,6 +118,16 @@ impl ValueSetT for ValueSetRestricted { self.set.iter().cloned().collect() } + fn generate_idx_sub_keys(&self) -> Vec { + let lower: Vec<_> = self.set.iter().map(|s| s.to_lowercase()).collect(); + let mut trigraphs: Vec<_> = lower.iter().flat_map(|v| trigraph_iter(v)).collect(); + + trigraphs.sort_unstable(); + trigraphs.dedup(); + + trigraphs.into_iter().map(String::from).collect() + } + fn syntax(&self) -> SyntaxType { unreachable!(); // SyntaxType::RestrictedString diff --git a/server/lib/src/valueset/ssh.rs b/server/lib/src/valueset/ssh.rs index 8b2235437..d5157200d 100644 --- a/server/lib/src/valueset/ssh.rs +++ b/server/lib/src/valueset/ssh.rs @@ -5,6 +5,7 @@ use crate::be::dbvalue::DbValueTaggedStringV1; use crate::prelude::*; use crate::repl::proto::ReplAttrV1; use crate::schema::SchemaAttribute; +use crate::utils::trigraph_iter; use crate::valueset::{DbValueSetV2, ValueSet}; use sshkey_attest::proto::PublicKey as SshPublicKey; @@ -125,6 +126,16 @@ impl ValueSetT for ValueSetSshKey { self.map.keys().cloned().collect() } + fn generate_idx_sub_keys(&self) -> Vec { + let lower: Vec<_> = self.map.keys().map(|s| s.to_lowercase()).collect(); + let mut trigraphs: Vec<_> = lower.iter().flat_map(|v| trigraph_iter(v)).collect(); + + trigraphs.sort_unstable(); + trigraphs.dedup(); + + trigraphs.into_iter().map(String::from).collect() + } + fn syntax(&self) -> SyntaxType { SyntaxType::SshKey } diff --git a/server/lib/src/valueset/utf8.rs b/server/lib/src/valueset/utf8.rs index d88a1efd9..cce731944 100644 --- a/server/lib/src/valueset/utf8.rs +++ b/server/lib/src/valueset/utf8.rs @@ -3,6 +3,7 @@ use std::collections::BTreeSet; use crate::prelude::*; use crate::repl::proto::ReplAttrV1; use crate::schema::SchemaAttribute; +use crate::utils::trigraph_iter; use crate::valueset::{DbValueSetV2, ValueSet}; #[derive(Debug, Clone)] @@ -121,6 +122,16 @@ impl ValueSetT for ValueSetUtf8 { self.set.iter().cloned().collect() } + fn generate_idx_sub_keys(&self) -> Vec { + let lower: Vec<_> = self.set.iter().map(|s| s.to_lowercase()).collect(); + let mut trigraphs: Vec<_> = lower.iter().flat_map(|v| trigraph_iter(v)).collect(); + + trigraphs.sort_unstable(); + trigraphs.dedup(); + + trigraphs.into_iter().map(String::from).collect() + } + fn syntax(&self) -> SyntaxType { SyntaxType::Utf8String } diff --git a/tools/cli/src/cli/group/mod.rs b/tools/cli/src/cli/group/mod.rs index ba27947b2..9af8c0f70 100644 --- a/tools/cli/src/cli/group/mod.rs +++ b/tools/cli/src/cli/group/mod.rs @@ -7,7 +7,7 @@ mod account_policy; impl GroupOpt { pub fn debug(&self) -> bool { match self { - GroupOpt::List(copt) => copt.debug, + GroupOpt::List(copt) | GroupOpt::Search { copt, .. } => copt.debug, GroupOpt::Get(gcopt) => gcopt.copt.debug, GroupOpt::SetEntryManagedBy { copt, .. } | GroupOpt::Create { copt, .. } => copt.debug, GroupOpt::Delete(gcopt) => gcopt.copt.debug, @@ -44,6 +44,22 @@ impl GroupOpt { Err(e) => handle_client_error(e, copt.output_mode), } } + GroupOpt::Search { copt, name } => { + let client = copt.to_client(OpType::Read).await; + match client.idm_group_search(name).await { + Ok(r) => match copt.output_mode { + OutputMode::Json => { + let r_attrs: Vec<_> = r.iter().map(|entry| &entry.attrs).collect(); + println!( + "{}", + serde_json::to_string(&r_attrs).expect("Failed to serialise json") + ); + } + OutputMode::Text => r.iter().for_each(|ent| println!("{}", ent)), + }, + Err(e) => handle_client_error(e, copt.output_mode), + } + } GroupOpt::Get(gcopt) => { let client = gcopt.copt.to_client(OpType::Read).await; // idm_group_get diff --git a/tools/cli/src/cli/person.rs b/tools/cli/src/cli/person.rs index 31287a50f..d5046c5f7 100644 --- a/tools/cli/src/cli/person.rs +++ b/tools/cli/src/cli/person.rs @@ -66,6 +66,7 @@ impl PersonOpt { AccountCertificate::Status { copt, .. } | AccountCertificate::Create { copt, .. } => copt.debug, }, + PersonOpt::Search { copt, .. } => copt.debug, } } @@ -283,6 +284,22 @@ impl PersonOpt { Err(e) => handle_client_error(e, copt.output_mode), } } + PersonOpt::Search { copt, account_id } => { + let client = copt.to_client(OpType::Read).await; + match client.idm_person_search(account_id).await { + Ok(r) => match copt.output_mode { + OutputMode::Json => { + let r_attrs: Vec<_> = r.iter().map(|entry| &entry.attrs).collect(); + println!( + "{}", + serde_json::to_string(&r_attrs).expect("Failed to serialise json") + ); + } + OutputMode::Text => r.iter().for_each(|ent| println!("{}", ent)), + }, + Err(e) => handle_client_error(e, copt.output_mode), + } + } PersonOpt::Update(aopt) => { let client = aopt.copt.to_client(OpType::Write).await; match client diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index d4c90a09f..053c5acf3 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -236,6 +236,14 @@ pub enum GroupOpt { /// View a specific group #[clap(name = "get")] Get(Named), + /// Search a group by name + #[clap(name = "search")] + Search { + /// The name of the group + name: String, + #[clap(flatten)] + copt: CommonOpt, + }, /// Create a new group #[clap(name = "create")] Create { @@ -606,6 +614,13 @@ pub enum PersonOpt { /// View a specific person #[clap(name = "get")] Get(AccountNamedOpt), + /// Search persons by name + #[clap(name = "search")] + Search { + account_id: String, + #[clap(flatten)] + copt: CommonOpt, + }, /// Update a specific person's attributes #[clap(name = "update")] Update(PersonUpdateOpt),