mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Substring Indexing (#2905)
This commit is contained in:
parent
a695e0d75f
commit
da7ed77dfa
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,16 +1790,19 @@ impl Entry<EntrySealed, EntryCommitted> {
|
|||
post_idx_keys.len()
|
||||
};
|
||||
|
||||
let mut added_vs = Vec::with_capacity(sz);
|
||||
let mut removed_vs = Vec::with_capacity(sz);
|
||||
|
||||
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();
|
||||
|
||||
let mut added_vs = Vec::with_capacity(sz);
|
||||
|
||||
let mut removed_vs = Vec::with_capacity(sz);
|
||||
|
||||
loop {
|
||||
match (pre, post) {
|
||||
(Some(a), Some(b)) => {
|
||||
|
@ -1807,12 +1835,13 @@ impl Entry<EntrySealed, EntryCommitted> {
|
|||
}
|
||||
}
|
||||
}
|
||||
} // 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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue