mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-24 04:57:00 +01:00
Implemented name to uuid
This commit is contained in:
parent
cbc675c7bb
commit
61335a1ae5
|
@ -24,5 +24,6 @@ pub enum OperationError {
|
||||||
InvalidDBState,
|
InvalidDBState,
|
||||||
InvalidRequestState,
|
InvalidRequestState,
|
||||||
InvalidState,
|
InvalidState,
|
||||||
|
InvalidEntryState,
|
||||||
BackendEngine,
|
BackendEngine,
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,13 +69,7 @@ impl SearchEvent {
|
||||||
pub fn from_request(request: SearchRequest) -> Self {
|
pub fn from_request(request: SearchRequest) -> Self {
|
||||||
SearchEvent {
|
SearchEvent {
|
||||||
internal: false,
|
internal: false,
|
||||||
filter: Filter::And(vec![
|
filter: Filter::new_ignore_hidden(Filter::from(&request.filter)),
|
||||||
Filter::AndNot(Box::new(Filter::Or(vec![
|
|
||||||
Filter::Eq("class".to_string(), "tombstone".to_string()),
|
|
||||||
Filter::Eq("class".to_string(), "recycled".to_string()),
|
|
||||||
]))),
|
|
||||||
Filter::from(&request.filter),
|
|
||||||
]),
|
|
||||||
class: (),
|
class: (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,10 +84,7 @@ impl SearchEvent {
|
||||||
|
|
||||||
pub fn from_rec_request(request: SearchRecycledRequest) -> Self {
|
pub fn from_rec_request(request: SearchRecycledRequest) -> Self {
|
||||||
SearchEvent {
|
SearchEvent {
|
||||||
filter: Filter::And(vec![
|
filter: Filter::new_recycled(Filter::from(&request.filter)),
|
||||||
Filter::Eq("class".to_string(), "recycled".to_string()),
|
|
||||||
Filter::from(&request.filter),
|
|
||||||
]),
|
|
||||||
internal: false,
|
internal: false,
|
||||||
class: (),
|
class: (),
|
||||||
}
|
}
|
||||||
|
@ -174,13 +165,7 @@ pub struct DeleteEvent {
|
||||||
impl DeleteEvent {
|
impl DeleteEvent {
|
||||||
pub fn from_request(request: DeleteRequest) -> Self {
|
pub fn from_request(request: DeleteRequest) -> Self {
|
||||||
DeleteEvent {
|
DeleteEvent {
|
||||||
filter: Filter::And(vec![
|
filter: Filter::new_ignore_hidden(Filter::from(&request.filter)),
|
||||||
Filter::AndNot(Box::new(Filter::Or(vec![
|
|
||||||
Filter::Eq("class".to_string(), "tombstone".to_string()),
|
|
||||||
Filter::Eq("class".to_string(), "recycled".to_string()),
|
|
||||||
]))),
|
|
||||||
Filter::from(&request.filter),
|
|
||||||
]),
|
|
||||||
internal: false,
|
internal: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,13 +196,7 @@ pub struct ModifyEvent {
|
||||||
impl ModifyEvent {
|
impl ModifyEvent {
|
||||||
pub fn from_request(request: ModifyRequest) -> Self {
|
pub fn from_request(request: ModifyRequest) -> Self {
|
||||||
ModifyEvent {
|
ModifyEvent {
|
||||||
filter: Filter::And(vec![
|
filter: Filter::new_ignore_hidden(Filter::from(&request.filter)),
|
||||||
Filter::AndNot(Box::new(Filter::Or(vec![
|
|
||||||
Filter::Eq("class".to_string(), "tombstone".to_string()),
|
|
||||||
Filter::Eq("class".to_string(), "recycled".to_string()),
|
|
||||||
]))),
|
|
||||||
Filter::from(&request.filter),
|
|
||||||
]),
|
|
||||||
modlist: ModifyList::from(&request.modlist),
|
modlist: ModifyList::from(&request.modlist),
|
||||||
internal: false,
|
internal: false,
|
||||||
}
|
}
|
||||||
|
@ -301,10 +280,7 @@ impl Message for ReviveRecycledEvent {
|
||||||
impl ReviveRecycledEvent {
|
impl ReviveRecycledEvent {
|
||||||
pub fn from_request(request: ReviveRecycledRequest) -> Self {
|
pub fn from_request(request: ReviveRecycledRequest) -> Self {
|
||||||
ReviveRecycledEvent {
|
ReviveRecycledEvent {
|
||||||
filter: Filter::And(vec![
|
filter: Filter::new_recycled(Filter::from(&request.filter)),
|
||||||
Filter::Eq("class".to_string(), "recycled".to_string()),
|
|
||||||
Filter::from(&request.filter),
|
|
||||||
]),
|
|
||||||
internal: false,
|
internal: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,25 @@ impl Filter<FilterValid> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Filter<FilterInvalid> {
|
impl Filter<FilterInvalid> {
|
||||||
|
pub fn new_ignore_hidden(inner: Filter<FilterInvalid>) -> Self {
|
||||||
|
// Create a new filter, that ignores hidden entries.
|
||||||
|
Filter::And(vec![
|
||||||
|
Filter::AndNot(Box::new(Filter::Or(vec![
|
||||||
|
Filter::Eq("class".to_string(), "tombstone".to_string()),
|
||||||
|
Filter::Eq("class".to_string(), "recycled".to_string()),
|
||||||
|
]))),
|
||||||
|
inner,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_recycled(inner: Filter<FilterInvalid>) -> Self {
|
||||||
|
// Create a filter that searches recycled items only.
|
||||||
|
Filter::And(vec![
|
||||||
|
Filter::Eq("class".to_string(), "recycled".to_string()),
|
||||||
|
inner,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate(
|
pub fn validate(
|
||||||
&self,
|
&self,
|
||||||
schema: &SchemaReadTransaction,
|
schema: &SchemaReadTransaction,
|
||||||
|
@ -96,7 +115,7 @@ impl Filter<FilterInvalid> {
|
||||||
Some(schema_a) => {
|
Some(schema_a) => {
|
||||||
let value_norm = schema_a.normalise_value(value);
|
let value_norm = schema_a.normalise_value(value);
|
||||||
schema_a
|
schema_a
|
||||||
.validate_value(value)
|
.validate_value(&value_norm)
|
||||||
// Okay, it worked, transform to a filter component
|
// Okay, it worked, transform to a filter component
|
||||||
.map(|_| Filter::Eq(attr_norm, value_norm))
|
.map(|_| Filter::Eq(attr_norm, value_norm))
|
||||||
// On error, pass the error back out.
|
// On error, pass the error back out.
|
||||||
|
@ -112,7 +131,7 @@ impl Filter<FilterInvalid> {
|
||||||
Some(schema_a) => {
|
Some(schema_a) => {
|
||||||
let value_norm = schema_a.normalise_value(value);
|
let value_norm = schema_a.normalise_value(value);
|
||||||
schema_a
|
schema_a
|
||||||
.validate_value(value)
|
.validate_value(&value_norm)
|
||||||
// Okay, it worked, transform to a filter component
|
// Okay, it worked, transform to a filter component
|
||||||
.map(|_| Filter::Sub(attr_norm, value_norm))
|
.map(|_| Filter::Sub(attr_norm, value_norm))
|
||||||
// On error, pass the error back out.
|
// On error, pass the error back out.
|
||||||
|
|
|
@ -1479,29 +1479,27 @@ mod tests {
|
||||||
let f_insense = Filter::Eq("class".to_string(), "AttributeType".to_string());
|
let f_insense = Filter::Eq("class".to_string(), "AttributeType".to_string());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
f_insense.validate(&schema),
|
f_insense.validate(&schema),
|
||||||
Err(SchemaError::InvalidAttributeSyntax)
|
Ok(Filter::Eq("class".to_string(), "attributetype".to_string()))
|
||||||
);
|
);
|
||||||
// Test the recursive structures validate
|
// Test the recursive structures validate
|
||||||
let f_or_empty = Filter::Or(Vec::new());
|
let f_or_empty = Filter::Or(Vec::new());
|
||||||
assert_eq!(f_or_empty.validate(&schema), Err(SchemaError::EmptyFilter));
|
assert_eq!(f_or_empty.validate(&schema), Err(SchemaError::EmptyFilter));
|
||||||
let f_or = Filter::Or(vec![Filter::Eq(
|
let f_or = Filter::Or(vec![Filter::Eq("secret".to_string(), "zzzz".to_string())]);
|
||||||
"class".to_string(),
|
|
||||||
"AttributeType".to_string(),
|
|
||||||
)]);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
f_or.validate(&schema),
|
f_or.validate(&schema),
|
||||||
Err(SchemaError::InvalidAttributeSyntax)
|
Err(SchemaError::InvalidAttributeSyntax)
|
||||||
);
|
);
|
||||||
let f_or_mult = Filter::And(vec![
|
let f_or_mult = Filter::And(vec![
|
||||||
Filter::Eq("class".to_string(), "attributetype".to_string()),
|
Filter::Eq("class".to_string(), "attributetype".to_string()),
|
||||||
Filter::Eq("class".to_string(), "AttributeType".to_string()),
|
Filter::Eq("secret".to_string(), "zzzzzzz".to_string()),
|
||||||
]);
|
]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
f_or_mult.validate(&schema),
|
f_or_mult.validate(&schema),
|
||||||
Err(SchemaError::InvalidAttributeSyntax)
|
Err(SchemaError::InvalidAttributeSyntax)
|
||||||
);
|
);
|
||||||
|
// Test mixed case attr name - this is a pass, due to normalisation
|
||||||
let f_or_ok = Filter::AndNot(Box::new(Filter::And(vec![
|
let f_or_ok = Filter::AndNot(Box::new(Filter::And(vec![
|
||||||
Filter::Eq("class".to_string(), "attributetype".to_string()),
|
Filter::Eq("Class".to_string(), "AttributeType".to_string()),
|
||||||
Filter::Sub("class".to_string(), "classtype".to_string()),
|
Filter::Sub("class".to_string(), "classtype".to_string()),
|
||||||
Filter::Pres("class".to_string()),
|
Filter::Pres("class".to_string()),
|
||||||
])));
|
])));
|
||||||
|
@ -1513,7 +1511,6 @@ mod tests {
|
||||||
Filter::Pres("class".to_string()),
|
Filter::Pres("class".to_string()),
|
||||||
]))))
|
]))))
|
||||||
);
|
);
|
||||||
// Test mixed case attr name - this is a pass, due to normalisation
|
|
||||||
println!("{}", audit);
|
println!("{}", audit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,22 +36,22 @@ pub trait QueryServerReadTransaction {
|
||||||
au: &mut AuditScope,
|
au: &mut AuditScope,
|
||||||
se: &SearchEvent,
|
se: &SearchEvent,
|
||||||
) -> Result<Vec<Entry<EntryValid, EntryCommitted>>, OperationError> {
|
) -> Result<Vec<Entry<EntryValid, EntryCommitted>>, OperationError> {
|
||||||
// How to get schema?
|
audit_log!(au, "search: filter -> {:?}", se.filter);
|
||||||
|
|
||||||
// This is an important security step because it prevents us from
|
// This is an important security step because it prevents us from
|
||||||
// performing un-indexed searches on attr's that don't exist in the
|
// performing un-indexed searches on attr's that don't exist in the
|
||||||
// server. This is why ExtensibleObject can only take schema that
|
// server. This is why ExtensibleObject can only take schema that
|
||||||
// exists in the server, not arbitrary attr names.
|
// exists in the server, not arbitrary attr names.
|
||||||
audit_log!(au, "search: filter -> {:?}", se.filter);
|
//
|
||||||
|
// This normalises and validates in a single step.
|
||||||
// TODO: Normalise the filter
|
|
||||||
|
|
||||||
// TODO: Validate the filter
|
|
||||||
let vf = match se.filter.validate(self.get_schema()) {
|
let vf = match se.filter.validate(self.get_schema()) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
// TODO: Do something with this error
|
// TODO: Do something with this error
|
||||||
Err(e) => return Err(OperationError::SchemaViolation(e)),
|
Err(e) => return Err(OperationError::SchemaViolation(e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
audit_log!(au, "search: valid filter -> {:?}", vf);
|
||||||
|
|
||||||
// TODO: Assert access control allows the filter and requested attrs.
|
// TODO: Assert access control allows the filter and requested attrs.
|
||||||
|
|
||||||
// TODO: Pre-search plugins
|
// TODO: Pre-search plugins
|
||||||
|
@ -91,6 +91,83 @@ pub trait QueryServerReadTransaction {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Should this actually be names_to_uuids and we do batches?
|
||||||
|
// In the initial design "no", we can always write a batched
|
||||||
|
// interface later.
|
||||||
|
//
|
||||||
|
// The main question is if we need association between the name and
|
||||||
|
// the request uuid - if we do, we need singular. If we don't, we can
|
||||||
|
// just do the batching.
|
||||||
|
//
|
||||||
|
// Filter conversion likely needs 1:1, due to and/or conversions
|
||||||
|
// but create/mod likely doesn't due to the nature of the attributes.
|
||||||
|
//
|
||||||
|
// In the end, singular is the simple and correct option, so lets do
|
||||||
|
// that first, and we can add batched (and cache!) later.
|
||||||
|
//
|
||||||
|
// Remember, we don't care if the name is invalid, because search
|
||||||
|
// will validate/normalise the filter we construct for us. COOL!
|
||||||
|
fn name_to_uuid(
|
||||||
|
&self,
|
||||||
|
audit: &mut AuditScope,
|
||||||
|
name: &String,
|
||||||
|
) -> Result<String, OperationError> {
|
||||||
|
// For now this just constructs a filter and searches, but later
|
||||||
|
// we could actually improve this to contact the backend and do
|
||||||
|
// index searches, completely bypassing id2entry.
|
||||||
|
|
||||||
|
// construct the filter
|
||||||
|
let filt = Filter::new_ignore_hidden(Filter::Eq("name".to_string(), name.clone()));
|
||||||
|
audit_log!(audit, "name_to_uuid: name -> {:?}", name);
|
||||||
|
|
||||||
|
// Internal search - DO NOT SEARCH TOMBSTONES AND RECYCLE
|
||||||
|
let res = match self.internal_search(audit, filt) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
audit_log!(audit, "name_to_uuid: results -- {:?}", res);
|
||||||
|
|
||||||
|
if res.len() == 0 {
|
||||||
|
// If result len == 0, error no such result
|
||||||
|
return Err(OperationError::NoMatchingEntries);
|
||||||
|
} else if res.len() >= 2 {
|
||||||
|
// if result len >= 2, error, invaid entry state.
|
||||||
|
return Err(OperationError::InvalidDBState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Is there a better solution here than this?
|
||||||
|
// Perhaps we could res.first, then unwrap the some
|
||||||
|
// for 0/1 case, but check len for >= 2 to eliminate that case.
|
||||||
|
let e = res.first().unwrap();
|
||||||
|
// Get the uuid from the entry. Again, check it exists, and only one.
|
||||||
|
let uuid_res = match e.get_ava(&String::from("uuid")) {
|
||||||
|
Some(vas) => match vas.first() {
|
||||||
|
Some(u) => u.clone(),
|
||||||
|
None => return Err(OperationError::InvalidEntryState),
|
||||||
|
},
|
||||||
|
None => return Err(OperationError::InvalidEntryState),
|
||||||
|
};
|
||||||
|
|
||||||
|
audit_log!(audit, "name_to_uuid: uuid <- {:?}", uuid_res);
|
||||||
|
|
||||||
|
Ok(uuid_res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uuid_to_name(&self, audit: &mut AuditScope, &String) -> Result<String, OperationError> {
|
||||||
|
// Construct filter
|
||||||
|
|
||||||
|
// Internal search - DO NOT SEARCH TOMBSTONES AND RECYCLE
|
||||||
|
|
||||||
|
// If result len == 0, error no such result
|
||||||
|
// if result len >= 2, error, invaid entry state.
|
||||||
|
|
||||||
|
// Get the name
|
||||||
|
|
||||||
|
// Return it.
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
// From internal, generate an exists event and dispatch
|
// From internal, generate an exists event and dispatch
|
||||||
fn internal_exists(
|
fn internal_exists(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1497,4 +1574,43 @@ mod tests {
|
||||||
assert!(server_txn.commit(audit).is_ok());
|
assert!(server_txn.commit(audit).is_ok());
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_qs_name_to_uuid() {
|
||||||
|
run_test!(|mut server: QueryServer, audit: &mut AuditScope| {
|
||||||
|
let mut server_txn = server.write();
|
||||||
|
|
||||||
|
let e1: Entry<EntryInvalid, EntryNew> = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"valid": null,
|
||||||
|
"state": null,
|
||||||
|
"attrs": {
|
||||||
|
"class": ["object", "person"],
|
||||||
|
"name": ["testperson1"],
|
||||||
|
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
|
||||||
|
"description": ["testperson"],
|
||||||
|
"displayname": ["testperson1"]
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let ce = CreateEvent::from_vec(vec![e1]);
|
||||||
|
let cr = server_txn.create(audit, &ce);
|
||||||
|
assert!(cr.is_ok());
|
||||||
|
|
||||||
|
// Name doesn't exist
|
||||||
|
let r1 = server_txn.name_to_uuid(audit, &String::from("testpers"));
|
||||||
|
assert!(r1.is_err());
|
||||||
|
// Name doesn't exist (not syntax normalised)
|
||||||
|
let r2 = server_txn.name_to_uuid(audit, &String::from("tEsTpErS"));
|
||||||
|
assert!(r2.is_err());
|
||||||
|
// Name does exist
|
||||||
|
let r3 = server_txn.name_to_uuid(audit, &String::from("testperson1"));
|
||||||
|
assert!(r3.is_ok());
|
||||||
|
// Name is not syntax normalised (but exists)
|
||||||
|
let r4 = server_txn.name_to_uuid(audit, &String::from("tEsTpErSoN1"));
|
||||||
|
println!("{:?}", r4);
|
||||||
|
assert!(r4.is_ok());
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue