Substring Indexing (#2905)

This commit is contained in:
Firstyear 2024-07-20 13:12:49 +10:00 committed by GitHub
parent a695e0d75f
commit da7ed77dfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 562 additions and 95 deletions

View file

@ -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<Vec<Entry>, 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

View file

@ -17,6 +17,11 @@ impl KanidmClient {
.await
}
pub async fn idm_person_search(&self, id: &str) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request(format!("/v1/person/_search/{}", id).as_str())
.await
}
pub async fn idm_person_account_create(
&self,
name: &str,

View file

@ -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,

View file

@ -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<ProtoEntry>, 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<ServerState>,
Path(id): Path<String>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
) -> Result<Json<Vec<ProtoEntry>>, 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<ProtoEntry>, 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<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Path(id): Path<String>,
) -> Result<Json<Vec<ProtoEntry>>, 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<ServerState> {
.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<ServerState> {
.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)

View file

@ -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

View file

@ -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(),

View file

@ -1646,7 +1646,11 @@ impl Entry<EntrySealed, EntryCommitted> {
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<EntrySealed, EntryCommitted> {
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<EntrySealed, EntryCommitted> {
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<EntrySealed, EntryCommitted> {
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<EntrySealed, EntryCommitted> {
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<EntrySealed, EntryCommitted> {
IndexType::Presence => {
// No action - we still are "present", so nothing to do!
}
IndexType::SubString => {}
};
// Return the diff
diff

View file

@ -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);

View file

@ -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,
},
);

View file

@ -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(),

View file

@ -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<char> for DistinctAlpha {
}
}
pub(crate) struct GraphemeClusterIter<'a> {
value: &'a str,
char_bounds: Vec<usize>,
window: usize,
range: Range<usize>,
}
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<usize>) {
let clusters = self.char_bounds.len().checked_sub(1).unwrap_or(0);
(clusters, Some(clusters))
}
}
pub(crate) fn trigraph_iter(value: &str) -> impl Iterator<Item = &str> {
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);
}
}

View file

@ -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<String> {
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,
}
}
}

View file

@ -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<String> {
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
}

View file

@ -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<String> {
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

View file

@ -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<String> {
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
}

View file

@ -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<String> {
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
}

View file

@ -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<String> {
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
}

View file

@ -132,6 +132,10 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
fn generate_idx_eq_keys(&self) -> Vec<String>;
fn generate_idx_sub_keys(&self) -> Vec<String> {
Vec::with_capacity(0)
}
fn syntax(&self) -> SyntaxType;
fn validate(&self, schema_attr: &SchemaAttribute) -> bool;

View file

@ -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<String> {
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

View file

@ -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<String> {
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
}

View file

@ -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<String> {
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
}

View file

@ -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

View file

@ -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

View file

@ -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),