From dddd04898cf7fdd2f9b134bbed238acfc365e134 Mon Sep 17 00:00:00 2001 From: William Brown Date: Wed, 14 Nov 2018 14:54:59 +1300 Subject: [PATCH] Schema aware create now works! --- src/be/mod.rs | 4 +- src/entry.rs | 86 ++++-- src/error.rs | 1 + src/filter.rs | 15 +- src/lib.rs | 2 - src/schema.rs | 805 ++++++++++++++++++++++++++++++++++++++++++-------- src/server.rs | 50 +++- 7 files changed, 796 insertions(+), 167 deletions(-) diff --git a/src/be/mod.rs b/src/be/mod.rs index 0987760ea..6d4b3f5d1 100644 --- a/src/be/mod.rs +++ b/src/be/mod.rs @@ -274,9 +274,7 @@ mod tests { assert_eq!(empty_result, Err(BackendError::EmptyRequest)); let mut e: Entry = Entry::new(); - e.add_ava(String::from("userid"), String::from("william")) - .unwrap(); - assert!(e.validate()); + e.add_ava(String::from("userid"), String::from("william")); let single_result = be.create(audit, &vec![e]); diff --git a/src/entry.rs b/src/entry.rs index e4c903b98..e63775e2a 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -88,27 +88,48 @@ impl Entry { // This should always work? It's only on validate that we'll build // a list of syntax violations ... - pub fn add_ava(&mut self, attr: String, value: String) -> Result<(), ()> { + // If this already exists, we silently drop the event? Is that an + // acceptable interface? + // Should value here actually be a &str? + pub fn add_ava(&mut self, attr: String, value: String) { // get_mut to access value + // How do we make this turn into an ok / err? self.attrs .entry(attr) - .and_modify(|v| v.push(value.clone())) + .and_modify(|v| { + // Here we need to actually do a check/binary search ... + v.binary_search(&value).map_err(|idx| { + // This cloning is to fix a borrow issue ... + v.insert(idx, value.clone()) + }); + }) .or_insert(vec![value]); - - Ok(()) } pub fn get_ava(&self, attr: &String) -> Option<&Vec> { self.attrs.get(attr) } - pub fn validate(&self) -> bool { - // We need access to the current system schema here now ... - true + pub fn attribute_pres(&self, attr: &str) -> bool { + // FIXME: Do we need to normalise attr name? + self.attrs.contains_key(attr) } - pub fn pres(&self, attr: &str) -> bool { - self.attrs.contains_key(attr) + pub fn attribute_equality(&self, attr: &str, value: &str) -> bool { + // Do a schema aware equality? + // Either we get schema passed in. + // OR we assume based on schema normalisation on the way in + // that the equality here of the raw values MUST be correct. + // If we do this, we likely need a DB normalise function ... + // The other issue is we have to normalise what's in the filter + // but that could be done *before* we get here? + + // FIXME: Make this binary_search + + self.attrs.get(attr).map_or(false, |v| { + v.iter() + .fold(false, |acc, av| if acc { acc } else { value == av }) + }) } pub fn classes(&self) -> EntryClasses { @@ -229,42 +250,55 @@ mod tests { #[test] fn test_user_basic() { let u: User = User::new("william", "William Brown"); - - println!("u: {:?}", u); - let d = serde_json::to_string_pretty(&u).unwrap(); - println!("d: {}", d.as_str()); - let u2: User = serde_json::from_str(d.as_str()).unwrap(); - - println!("u2: {:?}", u2); } #[test] fn test_entry_basic() { let mut e: Entry = Entry::new(); - e.add_ava(String::from("userid"), String::from("william")) - .unwrap(); - - assert!(e.validate()); + e.add_ava(String::from("userid"), String::from("william")); let d = serde_json::to_string_pretty(&e).unwrap(); + } - println!("d: {}", d.as_str()); + #[test] + fn test_entry_dup_value() { + // Schema doesn't matter here because we are duplicating a value + // it should fail! + + // We still probably need schema here anyway to validate what we + // are adding ... Or do we validate after the changes are made in + // total? + let mut e: Entry = Entry::new(); + e.add_ava(String::from("userid"), String::from("william")); + e.add_ava(String::from("userid"), String::from("william")); + + let values = e.get_ava(&String::from("userid")).unwrap(); + // Should only be one value! + assert_eq!(values.len(), 1) } #[test] fn test_entry_pres() { let mut e: Entry = Entry::new(); - e.add_ava(String::from("userid"), String::from("william")) - .unwrap(); + e.add_ava(String::from("userid"), String::from("william")); - assert!(e.validate()); + assert!(e.attribute_pres("userid")); + assert!(!e.attribute_pres("name")); + } - assert!(e.pres("userid")); - assert!(!e.pres("name")); + #[test] + fn test_entry_equality() { + let mut e: Entry = Entry::new(); + + e.add_ava(String::from("userid"), String::from("william")); + + assert!(e.attribute_equality("userid", "william")); + assert!(!e.attribute_equality("userid", "test")); + assert!(!e.attribute_equality("nonexist", "william")); } } diff --git a/src/error.rs b/src/error.rs index 8dd58eba2..585a30c4a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,4 +6,5 @@ pub enum SchemaError { MISSING_MUST_ATTRIBUTE, INVALID_ATTRIBUTE, INVALID_ATTRIBUTE_SYNTAX, + EMPTY_FILTER, } diff --git a/src/filter.rs b/src/filter.rs index 1459449e7..e75585a71 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -3,6 +3,7 @@ // entry to assert it matches. use super::entry::Entry; +use super::schema::Schema; use std::cmp::{Ordering, PartialOrd}; // Perhaps make these json serialisable. Certainly would make parsing @@ -20,7 +21,9 @@ pub enum Filter { } impl Filter { - fn optimise(mut self) -> Self { + // Does this need mut self? Aren't we returning + // a new copied filter? + fn optimise(&self) -> Self { // Apply optimisations to the filter // An easy way would be imple partialOrd // then do sort on the or/and/not @@ -33,12 +36,7 @@ impl Filter { // If an or/not/and condition has no items, remove it // // If its the root item? - self - } - - // In the future this will probably be used with schema ... - fn validate(&self) -> Result<(), ()> { - Ok(()) + self.clone() } // This is probably not safe, so it's for internal test cases @@ -62,7 +60,7 @@ impl Filter { Filter::Sub(_, _) => false, Filter::Pres(attr) => { // Given attr, is is present in the entry? - e.pres(attr.as_str()) + e.attribute_pres(attr.as_str()) } Filter::Or(_) => false, Filter::And(_) => false, @@ -130,6 +128,7 @@ impl PartialOrd for Filter { #[cfg(test)] mod tests { + use super::super::schema::Schema; use super::Filter; use serde_json; use std::cmp::{Ordering, PartialOrd}; diff --git a/src/lib.rs b/src/lib.rs index 3dacd81bd..ca3b26429 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,5 @@ - #![feature(try_from)] - extern crate serde; extern crate serde_json; #[macro_use] diff --git a/src/schema.rs b/src/schema.rs index cb65f571a..6c734427d 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,8 +1,10 @@ use super::entry::Entry; use super::error::SchemaError; +use super::filter::Filter; use std::collections::HashMap; // Apparently this is nightly only? use std::convert::TryFrom; +use std::str::FromStr; // representations of schema that confines object types, classes // and attributes. This ties in deeply with "Entry". @@ -26,10 +28,10 @@ pub enum IndexType { SUBSTRING, } -impl TryFrom for IndexType { +impl TryFrom<&str> for IndexType { type Error = (); - fn try_from(value: String) -> Result { + fn try_from(value: &str) -> Result { if value == "EQUALITY" { Ok(IndexType::EQUALITY) } else if value == "PRESENCE" { @@ -53,16 +55,30 @@ pub enum SyntaxType { INDEX_ID, } -impl TryFrom for IndexType { +impl TryFrom<&str> for SyntaxType { type Error = (); - fn try_from(value: String) -> Result { + fn try_from(value: &str) -> Result { + if value == "UTF8STRING" { + Ok(SyntaxType::UTF8STRING) + } else if value == "UTF8STRING_INSENSITIVE" { + Ok(SyntaxType::UTF8STRING_INSENSITIVE) + } else if value == "BOOLEAN" { + Ok(SyntaxType::BOOLEAN) + } else if value == "SYNTAX_ID" { + Ok(SyntaxType::SYNTAX_ID) + } else if value == "INDEX_ID" { + Ok(SyntaxType::INDEX_ID) + } else { + Err(()) + } } } #[derive(Debug, Clone)] pub struct SchemaAttribute { - class: Vec, + // Is this ... used? + // class: Vec, name: String, // Perhaps later add aliases? description: String, @@ -76,11 +92,110 @@ pub struct SchemaAttribute { impl SchemaAttribute { // Implement Equality, PartialOrd, Normalisation, // Validation. + fn validate_bool(&self, v: &String) -> Result<(), SchemaError> { + bool::from_str(v.as_str()) + .map_err(|_| SchemaError::INVALID_ATTRIBUTE_SYNTAX) + .map(|_| ()) + } + + fn validate_syntax(&self, v: &String) -> Result<(), SchemaError> { + SyntaxType::try_from(v.as_str()) + .map_err(|_| SchemaError::INVALID_ATTRIBUTE_SYNTAX) + .map(|_| ()) + } + + fn validate_index(&self, v: &String) -> Result<(), SchemaError> { + IndexType::try_from(v.as_str()) + .map_err(|_| SchemaError::INVALID_ATTRIBUTE_SYNTAX) + .map(|_| ()) + } + + fn validate_utf8string_insensitive(&self, v: &String) -> Result<(), SchemaError> { + // FIXME: Is there a way to do this that doesn't involve a copy? + let t = v.to_lowercase(); + if &t == v { + Ok(()) + } else { + Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX) + } + } + + pub fn validate_value(&self, v: &String) -> Result<(), SchemaError> { + match self.syntax { + SyntaxType::BOOLEAN => self.validate_bool(v), + SyntaxType::SYNTAX_ID => self.validate_syntax(v), + SyntaxType::INDEX_ID => self.validate_index(v), + SyntaxType::UTF8STRING_INSENSITIVE => self.validate_utf8string_insensitive(v), + _ => Ok(()), + } + } + + pub fn validate_ava(&self, ava: &Vec) -> Result<(), SchemaError> { + // Check multivalue + if self.multivalue == false && ava.len() > 1 { + return Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX); + }; + // If syntax, check the type is correct + match self.syntax { + SyntaxType::BOOLEAN => { + ava.iter().fold(Ok(()), |acc, v| { + if acc.is_ok() { + self.validate_bool(v) + } else { + // We got an error before, just skip the rest + acc + } + }) + } + SyntaxType::SYNTAX_ID => { + ava.iter().fold(Ok(()), |acc, v| { + // If acc is err, map will skip it. + if acc.is_ok() { + self.validate_syntax(v) + } else { + acc + } + }) + } + SyntaxType::INDEX_ID => { + ava.iter().fold(Ok(()), |acc, v| { + // If acc is err, map will skip it. + if acc.is_ok() { + self.validate_index(v) + } else { + acc + } + }) + } + SyntaxType::UTF8STRING_INSENSITIVE => { + ava.iter().fold(Ok(()), |acc, v| { + // If acc is err, map will skip it. + if acc.is_ok() { + self.validate_utf8string_insensitive(v) + } else { + acc + } + }) + } + _ => Ok(()), + } + } + + pub fn normalise_ava(&self, attr_type: String, attr_value: String) { + // Given some types, we can normalise them in sane and consistent + // ways. This is generally used in add_ava, and filter + // modification. + + // Given the attr_type load the schema_attribute + // given the syntax, normalise. + + } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SchemaClass { - class: Vec, + // Is this used? + // class: Vec, name: String, description: String, // This allows modification of system types to be extended in custom ways @@ -97,7 +212,7 @@ impl SchemaClass { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Schema { // We contain sets of classes and attributes. classes: HashMap, @@ -116,7 +231,7 @@ impl Schema { s.attributes.insert( String::from("class"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("class"), description: String::from("The set of classes defining an object"), system: true, @@ -129,7 +244,7 @@ impl Schema { s.attributes.insert( String::from("name"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("name"), description: String::from("The shortform name of an object"), system: true, @@ -142,7 +257,7 @@ impl Schema { s.attributes.insert( String::from("description"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("description"), description: String::from("A description of an attribute, object or class"), system: true, @@ -155,7 +270,7 @@ impl Schema { s.attributes.insert( String::from("system"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("system"), description: String::from( "Is this object or attribute provided from the core system?", @@ -168,7 +283,7 @@ impl Schema { }, ); s.attributes.insert(String::from("secret"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("secret"), description: String::from("If true, this value is always hidden internally to the server, even beyond access controls."), system: true, @@ -178,7 +293,7 @@ impl Schema { syntax: SyntaxType::BOOLEAN, }); s.attributes.insert(String::from("multivalue"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("multivalue"), description: String::from("If true, this attribute is able to store multiple values rather than just a single value."), system: true, @@ -190,7 +305,7 @@ impl Schema { s.attributes.insert( String::from("index"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("index"), description: String::from( "Describe the indexes to apply to instances of this attribute.", @@ -205,7 +320,7 @@ impl Schema { s.attributes.insert( String::from("syntax"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("syntax"), description: String::from( "Describe the syntax of this attribute. This affects indexing and sorting.", @@ -220,7 +335,7 @@ impl Schema { s.attributes.insert( String::from("systemmay"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("systemmay"), description: String::from( "A list of system provided optional attributes this class can store.", @@ -235,7 +350,7 @@ impl Schema { s.attributes.insert( String::from("may"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("may"), description: String::from( "A user modifiable list of optional attributes this class can store.", @@ -250,7 +365,7 @@ impl Schema { s.attributes.insert( String::from("systemmust"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("systemmust"), description: String::from( "A list of system provided required attributes this class must store.", @@ -265,7 +380,7 @@ impl Schema { s.attributes.insert( String::from("must"), SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("must"), description: String::from( "A user modifiable list of required attributes this class must store.", @@ -281,10 +396,10 @@ impl Schema { s.classes.insert( String::from("attributetype"), SchemaClass { - class: vec![String::from("classtype")], + // class: vec![String::from("classtype")], name: String::from("attributetype"), description: String::from("Definition of a schema attribute"), - systemmay: vec![String::from("index"), String::from("description")], + systemmay: vec![String::from("index")], may: vec![], systemmust: vec![ String::from("class"), @@ -293,6 +408,7 @@ impl Schema { String::from("secret"), String::from("multivalue"), String::from("syntax"), + String::from("description"), ], must: vec![], }, @@ -300,25 +416,28 @@ impl Schema { s.classes.insert( String::from("classtype"), SchemaClass { - class: vec![String::from("classtype")], + // class: vec![String::from("classtype")], name: String::from("classtype"), description: String::from("Definition of a schema classtype"), systemmay: vec![ - String::from("description"), String::from("systemmay"), String::from("may"), String::from("systemmust"), String::from("must"), ], may: vec![], - systemmust: vec![String::from("class"), String::from("name")], + systemmust: vec![ + String::from("class"), + String::from("name"), + String::from("description"), + ], must: vec![], }, ); s.classes.insert( String::from("extensibleobject"), SchemaClass { - class: vec![String::from("classtype")], + // class: vec![String::from("classtype")], name: String::from("extensibleobject"), description: String::from("A class type that turns off all rules ..."), systemmay: vec![], @@ -331,6 +450,138 @@ impl Schema { s } + // This shouldn't fail? + pub fn bootstrap_core(&mut self) { + // This will create a set of sane, system core schema that we can use + // main types are users, groups + + // Create attributes + // displayname // single + self.attributes.insert( + String::from("displayname"), + SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("displayname"), + description: String::from("The publicly visible display name of this person"), + system: true, + secret: false, + multivalue: false, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::UTF8STRING, + }, + ); + // name // single + // mail // multi + self.attributes.insert( + String::from("mail"), + SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("mail"), + description: String::from("mail addresses of the object"), + system: true, + secret: false, + multivalue: true, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::UTF8STRING, + }, + ); + // memberof // multi + self.attributes.insert( + String::from("memberof"), + SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("memberof"), + description: String::from("reverse group membership of the object"), + system: true, + secret: false, + multivalue: true, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }, + ); + // ssh_publickey // multi + self.attributes.insert( + String::from("ssh_publickey"), + SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("ssh_publickey"), + description: String::from("SSH public keys of the object"), + system: true, + secret: false, + multivalue: true, + index: vec![], + syntax: SyntaxType::UTF8STRING, + }, + ); + // password // secret, multi + self.attributes.insert( + String::from("password"), + SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("password"), + description: String::from( + "password hash material of the object for authentication", + ), + system: true, + secret: true, + multivalue: true, + index: vec![], + syntax: SyntaxType::UTF8STRING, + }, + ); + // + // member + self.attributes.insert( + String::from("member"), + SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("member"), + description: String::from("List of members of the group"), + system: true, + secret: false, + multivalue: true, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }, + ); + + // Create the classes that use it + // person + self.classes.insert( + String::from("person"), + SchemaClass { + name: String::from("person"), + description: String::from("Object representation of a person"), + systemmay: vec![ + String::from("description"), + String::from("mail"), + String::from("ssh_publickey"), + String::from("memberof"), + String::from("password"), + ], + may: vec![], + systemmust: vec![ + String::from("class"), + String::from("name"), + String::from("displayname"), + ], + must: vec![], + }, + ); + // group + self.classes.insert( + String::from("group"), + SchemaClass { + name: String::from("group"), + description: String::from("Object representation of a group"), + systemmay: vec![String::from("description"), String::from("member")], + may: vec![], + systemmust: vec![String::from("class"), String::from("name")], + must: vec![], + }, + ); + } + pub fn validate(&self) -> Result<(), ()> { // FIXME: How can we make this return a proper result? // @@ -432,11 +683,15 @@ impl Schema { // Check that any other attributes are in may // for each attr on the object, check it's in the may+must set for (attr_name, avas) in entry.avas() { - println!("AVAS {:?} : {:?}", attr_name, avas); match self.attributes.get(attr_name) { Some(a_schema) => { // Now, for each type we do a *full* check of the syntax // and validity of the ava. + let r = a_schema.validate_ava(avas); + // FIXME: This block could be more functional + if r.is_err() { + return r; + } } None => { if !extensible { @@ -449,39 +704,129 @@ impl Schema { // Well, we got here, so okay! Ok(()) } + + // Normalise also validates? + pub fn normalise_entry(&mut self) {} + + // This needs to be recursive? + pub fn validate_filter(&self, filt: &Filter) -> Result<(), SchemaError> { + match filt { + Filter::Eq(attr, value) => match self.attributes.get(attr) { + Some(schema_a) => schema_a.validate_value(value), + None => Err(SchemaError::INVALID_ATTRIBUTE), + }, + Filter::Sub(attr, value) => match self.attributes.get(attr) { + Some(schema_a) => schema_a.validate_value(value), + None => Err(SchemaError::INVALID_ATTRIBUTE), + }, + Filter::Pres(attr) => { + // This could be better as a contains_key + // because we never use the value + match self.attributes.get(attr) { + Some(_) => Ok(()), + None => Err(SchemaError::INVALID_ATTRIBUTE), + } + } + Filter::Or(filters) => { + // This should never happen because + // optimising should remove them as invalid parts? + if filters.len() == 0 { + return Err(SchemaError::EMPTY_FILTER); + }; + filters.iter().fold(Ok(()), |acc, filt| { + if acc.is_ok() { + self.validate_filter(filt) + } else { + acc + } + }) + } + Filter::And(filters) => { + // This should never happen because + // optimising should remove them as invalid parts? + if filters.len() == 0 { + return Err(SchemaError::EMPTY_FILTER); + }; + filters.iter().fold(Ok(()), |acc, filt| { + if acc.is_ok() { + self.validate_filter(filt) + } else { + acc + } + }) + } + Filter::Not(filters) => { + // This should never happen because + // optimising should remove them as invalid parts? + if filters.len() == 0 { + return Err(SchemaError::EMPTY_FILTER); + }; + filters.iter().fold(Ok(()), |acc, filt| { + if acc.is_ok() { + self.validate_filter(filt) + } else { + acc + } + }) + } + } + } + + // Normalise *does not* validate. + // Normalise just fixes some possible common issues, but it + // can't fix *everything* possibly wrong ... + pub fn normalise_filter(&mut self) {} } #[cfg(test)] mod tests { - use std::convert::TryFrom; use super::super::entry::Entry; use super::super::error::SchemaError; + use super::super::filter::Filter; use super::{IndexType, Schema, SchemaAttribute, SchemaClass, SyntaxType}; + use serde_json; + use std::convert::TryFrom; #[test] fn test_schema_index_tryfrom() { - let r1 = IndexType::try_from(String::from("EQUALITY")); + let r1 = IndexType::try_from("EQUALITY"); assert_eq!(r1, Ok(IndexType::EQUALITY)); - let r2 = IndexType::try_from(String::from("PRESENCE")); + let r2 = IndexType::try_from("PRESENCE"); assert_eq!(r2, Ok(IndexType::PRESENCE)); - let r3 = IndexType::try_from(String::from("SUBSTRING")); + let r3 = IndexType::try_from("SUBSTRING"); assert_eq!(r3, Ok(IndexType::SUBSTRING)); - let r4 = IndexType::try_from(String::from("thaoeusaneuh")); + let r4 = IndexType::try_from("thaoeusaneuh"); assert_eq!(r4, Err(())); } #[test] fn test_schema_syntax_tryfrom() { - + let r1 = SyntaxType::try_from("UTF8STRING"); + assert_eq!(r1, Ok(SyntaxType::UTF8STRING)); + + let r2 = SyntaxType::try_from("UTF8STRING_INSENSITIVE"); + assert_eq!(r2, Ok(SyntaxType::UTF8STRING_INSENSITIVE)); + + let r3 = SyntaxType::try_from("BOOLEAN"); + assert_eq!(r3, Ok(SyntaxType::BOOLEAN)); + + let r4 = SyntaxType::try_from("SYNTAX_ID"); + assert_eq!(r4, Ok(SyntaxType::SYNTAX_ID)); + + let r5 = SyntaxType::try_from("INDEX_ID"); + assert_eq!(r5, Ok(SyntaxType::INDEX_ID)); + + let r6 = SyntaxType::try_from("zzzzantheou"); + assert_eq!(r6, Err(())); } #[test] fn test_schema_attribute_simple() { let class_attribute = SchemaAttribute { - class: vec![String::from("attributetype")], + // class: vec![String::from("attributetype")], name: String::from("class"), description: String::from("The set of classes defining an object"), system: true, @@ -490,7 +835,98 @@ mod tests { index: vec![IndexType::EQUALITY], syntax: SyntaxType::UTF8STRING_INSENSITIVE, }; - // Test basic functions of simple attributes + + // Test schemaAttribute validation of types. + + // Test single value string + let single_value_string = SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("single_value"), + description: String::from(""), + system: true, + secret: false, + multivalue: false, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }; + + let r1 = single_value_string.validate_ava(&vec![String::from("test")]); + assert_eq!(r1, Ok(())); + + let r2 = + single_value_string.validate_ava(&vec![String::from("test1"), String::from("test2")]); + assert_eq!(r2, Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX)); + + // test multivalue string, boolean + + let multi_value_string = SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("mv_string"), + description: String::from(""), + system: true, + secret: false, + multivalue: true, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::UTF8STRING, + }; + + let r5 = + multi_value_string.validate_ava(&vec![String::from("test1"), String::from("test2")]); + assert_eq!(r5, Ok(())); + + let multi_value_boolean = SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("mv_bool"), + description: String::from(""), + system: true, + secret: false, + multivalue: true, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::BOOLEAN, + }; + + let r3 = + multi_value_boolean.validate_ava(&vec![String::from("test1"), String::from("test2")]); + assert_eq!(r3, Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX)); + + let r4 = + multi_value_boolean.validate_ava(&vec![String::from("true"), String::from("false")]); + assert_eq!(r4, Ok(())); + + // syntax_id and index_type values + let single_value_syntax = SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("sv_syntax"), + description: String::from(""), + system: true, + secret: false, + multivalue: false, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::SYNTAX_ID, + }; + + let r6 = single_value_syntax.validate_ava(&vec![String::from("UTF8STRING")]); + assert_eq!(r6, Ok(())); + + let r7 = single_value_syntax.validate_ava(&vec![String::from("thaeountaheu")]); + assert_eq!(r7, Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX)); + + let single_value_index = SchemaAttribute { + // class: vec![String::from("attributetype")], + name: String::from("sv_index"), + description: String::from(""), + system: true, + secret: false, + multivalue: false, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::INDEX_ID, + }; + // + let r8 = single_value_index.validate_ava(&vec![String::from("EQUALITY")]); + assert_eq!(r8, Ok(())); + + let r9 = single_value_index.validate_ava(&vec![String::from("thaeountaheu")]); + assert_eq!(r9, Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX)); } #[test] @@ -515,99 +951,100 @@ mod tests { // Given an entry, assert it's schema is valid // We do let schema = Schema::new(); - let mut e_no_class: Entry = Entry::new(); + let e_no_class: Entry = serde_json::from_str( + r#"{ + "attrs": {} + }"#, + ) + .unwrap(); + assert_eq!( schema.validate_entry(&e_no_class), Err(SchemaError::INVALID_CLASS) ); - let mut e_bad_class: Entry = Entry::new(); - e_bad_class - .add_ava(String::from("class"), String::from("zzzzzz")) - .unwrap(); + let e_bad_class: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["zzzzzz"] + } + }"#, + ) + .unwrap(); assert_eq!( schema.validate_entry(&e_bad_class), Err(SchemaError::INVALID_CLASS) ); - let mut e_attr_invalid: Entry = Entry::new(); - e_attr_invalid - .add_ava(String::from("class"), String::from("attributetype")) - .unwrap(); + let e_attr_invalid: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["attributetype"] + } + }"#, + ) + .unwrap(); assert_eq!( schema.validate_entry(&e_attr_invalid), Err(SchemaError::MISSING_MUST_ATTRIBUTE) ); - let mut e_attr_invalid_may: Entry = Entry::new(); - e_attr_invalid_may - .add_ava(String::from("class"), String::from("attributetype")) - .unwrap(); - e_attr_invalid_may - .add_ava(String::from("name"), String::from("testattr")) - .unwrap(); - e_attr_invalid_may - .add_ava(String::from("system"), String::from("false")) - .unwrap(); - e_attr_invalid_may - .add_ava(String::from("secret"), String::from("false")) - .unwrap(); - e_attr_invalid_may - .add_ava(String::from("multivalue"), String::from("false")) - .unwrap(); - e_attr_invalid_may - .add_ava(String::from("syntax"), String::from("UTF8STRING")) - .unwrap(); - // This is the invalid one - e_attr_invalid_may - .add_ava(String::from("zzzz"), String::from("zzzz")) - .unwrap(); + let e_attr_invalid_may: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["attributetype"], + "name": ["testattr"], + "description": ["testattr"], + "system": ["false"], + "secret": ["false"], + "multivalue": ["false"], + "syntax": ["UTF8STRING"], + "zzzzz": ["zzzz"] + } + }"#, + ) + .unwrap(); assert_eq!( schema.validate_entry(&e_attr_invalid_may), Err(SchemaError::INVALID_ATTRIBUTE) ); - let mut e_attr_invalid_syn: Entry = Entry::new(); - e_attr_invalid_syn - .add_ava(String::from("class"), String::from("attributetype")) - .unwrap(); - e_attr_invalid_syn - .add_ava(String::from("name"), String::from("testattr")) - .unwrap(); - e_attr_invalid_syn - .add_ava(String::from("system"), String::from("false")) - .unwrap(); - e_attr_invalid_syn - .add_ava(String::from("secret"), String::from("false")) - .unwrap(); - // This is the invalid one - e_attr_invalid_syn - .add_ava(String::from("multivalue"), String::from("zzzz")) - .unwrap(); - e_attr_invalid_syn - .add_ava(String::from("syntax"), String::from("UTF8STRING")) - .unwrap(); + let e_attr_invalid_syn: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["attributetype"], + "name": ["testattr"], + "description": ["testattr"], + "system": ["false"], + "secret": ["false"], + "multivalue": ["zzzzz"], + "syntax": ["UTF8STRING"] + } + }"#, + ) + .unwrap(); assert_eq!( schema.validate_entry(&e_attr_invalid_syn), Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX) ); - let mut e_ok: Entry = Entry::new(); - e_ok.add_ava(String::from("class"), String::from("attributetype")) - .unwrap(); - e_ok.add_ava(String::from("name"), String::from("testattr")) - .unwrap(); - e_ok.add_ava(String::from("system"), String::from("false")) - .unwrap(); - e_ok.add_ava(String::from("secret"), String::from("false")) - .unwrap(); - e_ok.add_ava(String::from("multivalue"), String::from("true")) - .unwrap(); - e_ok.add_ava(String::from("syntax"), String::from("UTF8STRING")) - .unwrap(); + let e_ok: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["attributetype"], + "name": ["testattr"], + "description": ["testattr"], + "system": ["false"], + "secret": ["false"], + "multivalue": ["true"], + "syntax": ["UTF8STRING"] + } + }"#, + ) + .unwrap(); assert_eq!(schema.validate_entry(&e_ok), Ok(())); } @@ -616,39 +1053,167 @@ mod tests { fn test_schema_extensible() { let schema = Schema::new(); // Just because you are extensible, doesn't mean you can be lazy - let mut e_extensible_bad: Entry = Entry::new(); - e_extensible_bad - .add_ava(String::from("class"), String::from("extensibleobject")) - .unwrap(); - // Secret is a boolean type - e_extensible_bad - .add_ava(String::from("secret"), String::from("zzzz")) - .unwrap(); + + let e_extensible_bad: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["extensibleobject"], + "secret": ["zzzz"] + } + }"#, + ) + .unwrap(); assert_eq!( schema.validate_entry(&e_extensible_bad), Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX) ); - let mut e_extensible: Entry = Entry::new(); - e_extensible - .add_ava(String::from("class"), String::from("extensibleobject")) - .unwrap(); - e_extensible - .add_ava(String::from("zzzz"), String::from("zzzz")) - .unwrap(); + let e_extensible: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["extensibleobject"], + "secret": ["true"] + } + }"#, + ) + .unwrap(); /* Is okay because extensible! */ assert_eq!(schema.validate_entry(&e_extensible), Ok(())); } - #[test] - fn test_schema_custom() { - // Validate custom schema entries - } - #[test] fn test_schema_loading() { // Validate loading schema from entries } + + #[test] + fn test_schema_bootstrap() { + let mut schema = Schema::new(); + schema.bootstrap_core(); + + // now test some entries + let e_person: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["person"], + "name": ["testperson"], + "description": ["testperson"], + "displayname": ["testperson"] + } + }"#, + ) + .unwrap(); + assert_eq!(schema.validate_entry(&e_person), Ok(())); + + let e_group: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["group"], + "name": ["testgroup"], + "description": ["testperson"] + } + }"#, + ) + .unwrap(); + assert_eq!(schema.validate_entry(&e_group), Ok(())); + } + + #[test] + fn test_schema_filter_validation() { + let mut schema = Schema::new(); + // Test mixed case attr name + let f_mixed: Filter = serde_json::from_str( + r#"{ + "Eq": [ + "ClAsS", "attributetype" + ] + }"#, + ) + .unwrap(); + assert_eq!( + schema.validate_filter(&f_mixed), + Err(SchemaError::INVALID_ATTRIBUTE) + ); + // test syntax of bool + let f_bool: Filter = serde_json::from_str( + r#"{ + "Eq": [ + "secret", "zzzz" + ] + }"#, + ) + .unwrap(); + assert_eq!( + schema.validate_filter(&f_bool), + Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX) + ); + // test insensitise values + let f_insense: Filter = serde_json::from_str( + r#"{ + "Eq": [ + "class", "AttributeType" + ] + }"#, + ) + .unwrap(); + assert_eq!( + schema.validate_filter(&f_insense), + Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX) + ); + // Test the recursive structures validate + let f_or_empty: Filter = serde_json::from_str( + r#"{ + "Or": [] + }"#, + ) + .unwrap(); + assert_eq!( + schema.validate_filter(&f_or_empty), + Err(SchemaError::EMPTY_FILTER) + ); + let f_or: Filter = serde_json::from_str( + r#"{ + "Or": [ + { "Eq": ["class", "AttributeType"] } + ] + }"#, + ) + .unwrap(); + assert_eq!( + schema.validate_filter(&f_or), + Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX) + ); + let f_or_mult: Filter = serde_json::from_str( + r#"{ + "Or": [ + { "Eq": ["class", "attributetype"] }, + { "Eq": ["class", "AttributeType"] } + ] + }"#, + ) + .unwrap(); + assert_eq!( + schema.validate_filter(&f_or_mult), + Err(SchemaError::INVALID_ATTRIBUTE_SYNTAX) + ); + let f_or_ok: Filter = serde_json::from_str( + r#"{ + "Or": [ + { "Eq": ["class", "attributetype"] }, + { "Eq": ["class", "classtype"] } + ] + }"#, + ) + .unwrap(); + assert_eq!(schema.validate_filter(&f_or_ok), Ok(())); + } + + #[test] + fn test_schema_filter_normalisation() { + // Test mixed case attr name + // test syntax of bool + // test normalise of insensitive strings + } } diff --git a/src/server.rs b/src/server.rs index 3f00dae8a..e705620f7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -5,6 +5,7 @@ use be::Backend; use entry::Entry; use event::{CreateEvent, EventResult, SearchEvent}; use log::EventLog; +use schema::Schema; pub fn start(log: actix::Addr, path: &str, threads: usize) -> actix::Addr { let mut audit = AuditEvent::new(); @@ -12,12 +13,16 @@ pub fn start(log: actix::Addr, path: &str, threads: usize) -> actix::A // Create the BE connection // probably need a config type soon .... let be = Backend::new(&mut audit, path); + let mut schema = Schema::new(); + schema.bootstrap_core(); // now we clone it out in the startup I think // Should the be need a log clone ref? or pass it around? // it probably needs it ... audit.end_event("server_new"); log.do_send(audit); - SyncArbiter::start(threads, move || QueryServer::new(log.clone(), be.clone())) + SyncArbiter::start(threads, move || { + QueryServer::new(log.clone(), be.clone(), schema.clone()) + }) } // This is the core of the server. It implements all @@ -35,12 +40,17 @@ pub struct QueryServer { // I think the BE is build, configured and cloned? Maybe Backend // is a wrapper type to Arc or something. be: Backend, + schema: Schema, } impl QueryServer { - pub fn new(log: actix::Addr, be: Backend) -> Self { + pub fn new(log: actix::Addr, be: Backend, schema: Schema) -> Self { log_event!(log, "Starting query worker ..."); - QueryServer { log: log, be: be } + QueryServer { + log: log, + be: be, + schema: schema, + } } // Actually conduct a search request @@ -59,6 +69,18 @@ impl QueryServer { pub fn create(&mut self, au: &mut AuditEvent, ce: &CreateEvent) -> Result<(), ()> { // Start a txn // Run any pre checks + + let r = ce.entries.iter().fold(Ok(()), |acc, e| { + if acc.is_ok() { + self.schema.validate_entry(e).map_err(|_| ()) + } else { + acc + } + }); + if r.is_err() { + return r; + } + // We may change from ce.entries later to something else? match self.be.create(au, &ce.entries) { Ok(_) => Ok(()), @@ -146,6 +168,7 @@ mod tests { use super::super::event::{CreateEvent, SearchEvent}; use super::super::filter::Filter; use super::super::log; + use super::super::schema::Schema; use super::super::server::QueryServer; macro_rules! run_test { @@ -155,7 +178,9 @@ mod tests { let test_log = log::start(); let be = Backend::new(&mut audit, ""); - let test_server = QueryServer::new(test_log.clone(), be); + let mut schema = Schema::new(); + schema.bootstrap_core(); + let test_server = QueryServer::new(test_log.clone(), be, schema); // Could wrap another future here for the future::ok bit... let fut = $test_fn(test_log.clone(), test_server, &mut audit); @@ -174,14 +199,22 @@ mod tests { #[test] fn test_be_create_user() { run_test!(|_log, mut server: QueryServer, audit: &mut AuditEvent| { - let filt = Filter::Pres(String::from("userid")); + let filt = Filter::Pres(String::from("name")); let se1 = SearchEvent::new(filt.clone()); let se2 = SearchEvent::new(filt); - let mut e: Entry = Entry::new(); - e.add_ava(String::from("userid"), String::from("william")) - .unwrap(); + let e: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["person"], + "name": ["testperson"], + "description": ["testperson"], + "displayname": ["testperson"] + } + }"#, + ) + .unwrap(); let expected = vec![e]; @@ -194,6 +227,7 @@ mod tests { assert!(cr.is_ok()); let r2 = server.search(audit, &se2).unwrap(); + println!("--> {:?}", r2); assert!(r2.len() == 1); assert_eq!(r2, expected);