diff --git a/src/entry.rs b/src/entry.rs index 8e33e9d08..e4c903b98 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -1,5 +1,8 @@ // use serde_json::{Error, Value}; +use std::collections::btree_map::Iter as BTreeIter; use std::collections::BTreeMap; +use std::marker::PhantomData; +use std::slice::Iter as SliceIter; // make a trait entry for everything to adhere to? // * How to get indexs out? @@ -28,6 +31,49 @@ use std::collections::BTreeMap; // } // +pub struct EntryClasses<'a> { + inner: Option>, + // _p: &'a PhantomData<()>, +} + +impl<'a> Iterator for EntryClasses<'a> { + type Item = &'a String; + + #[inline] + fn next(&mut self) -> Option<(&'a String)> { + match self.inner.iter_mut().next() { + Some(i) => i.next(), + None => None, + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self.inner.iter().next() { + Some(i) => i.size_hint(), + None => (0, None), + } + } +} + +pub struct EntryAvas<'a> { + inner: BTreeIter<'a, String, Vec>, +} + +impl<'a> Iterator for EntryAvas<'a> { + type Item = (&'a String, &'a Vec); + + #[inline] + fn next(&mut self) -> Option<(&'a String, &'a Vec)> { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct Entry { attrs: BTreeMap>, @@ -52,6 +98,10 @@ impl Entry { 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 @@ -60,6 +110,21 @@ impl Entry { pub fn pres(&self, attr: &str) -> bool { self.attrs.contains_key(attr) } + + pub fn classes(&self) -> EntryClasses { + // Get the class vec, if any? + // How do we indicate "empty?" + // FIXME: Actually handle this error ... + let c = self.attrs.get("class").map(|c| c.iter()); + EntryClasses { inner: c } + } + + pub fn avas(&self) -> EntryAvas { + // Get all attr:value pairs. + EntryAvas { + inner: self.attrs.iter(), + } + } } impl Clone for Entry { @@ -149,8 +214,10 @@ impl User { // We have to sort vecs ... // Is there a way to call this on serialise? - fn validate() -> Result<(), ()> { - Err(()) + fn validate(&self) -> Result<(), ()> { + // Given a schema, validate our object is sane. + + Ok(()) } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 000000000..8dd58eba2 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,9 @@ +#[derive(Debug, PartialEq)] +pub enum SchemaError { + NOT_IMPLEMENTED, + INVALID_CLASS, + // FIXME: Is there a way to say what we are missing on error? + MISSING_MUST_ATTRIBUTE, + INVALID_ATTRIBUTE, + INVALID_ATTRIBUTE_SYNTAX, +} diff --git a/src/lib.rs b/src/lib.rs index 837d508f3..3dacd81bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,7 @@ + +#![feature(try_from)] + + extern crate serde; extern crate serde_json; #[macro_use] @@ -24,8 +28,9 @@ pub mod log; mod audit; mod be; pub mod entry; +pub mod error; pub mod event; pub mod filter; pub mod proto; -pub mod server; pub mod schema; +pub mod server; diff --git a/src/schema.rs b/src/schema.rs index 62311667c..cb65f571a 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,4 +1,8 @@ +use super::entry::Entry; +use super::error::SchemaError; use std::collections::HashMap; +// Apparently this is nightly only? +use std::convert::TryFrom; // representations of schema that confines object types, classes // and attributes. This ties in deeply with "Entry". @@ -8,34 +12,92 @@ use std::collections::HashMap; // In the future this will parse/read it's schema from the db // but we have to bootstrap with some core types. +#[derive(Debug, PartialEq)] +enum Ternary { + Empty, + True, + False, +} + +#[derive(Debug, Clone, PartialEq)] pub enum IndexType { EQUALITY, PRESENCE, SUBSTRING, } -pub enum SyntaxType { - UTF8STRING, +impl TryFrom for IndexType { + type Error = (); + + fn try_from(value: String) -> Result { + if value == "EQUALITY" { + Ok(IndexType::EQUALITY) + } else if value == "PRESENCE" { + Ok(IndexType::PRESENCE) + } else if value == "SUBSTRING" { + Ok(IndexType::SUBSTRING) + } else { + Err(()) + } + } } +#[derive(Debug, Clone, PartialEq)] +pub enum SyntaxType { + // We need an insensitive string type too ... + // We also need to "self host" a syntax type, and index type + UTF8STRING, + UTF8STRING_INSENSITIVE, + BOOLEAN, + SYNTAX_ID, + INDEX_ID, +} + +impl TryFrom for IndexType { + type Error = (); + + fn try_from(value: String) -> Result { + } +} + +#[derive(Debug, Clone)] pub struct SchemaAttribute { + class: Vec, name: String, + // Perhaps later add aliases? description: String, system: bool, + secret: bool, multivalue: bool, index: Vec, syntax: SyntaxType, } -pub struct SchemaClass { - name: String, - descriptions: String, - systemmay: Vec, - may: Vec, - systemmust: Vec, - must: Vec, +impl SchemaAttribute { + // Implement Equality, PartialOrd, Normalisation, + // Validation. } +#[derive(Debug)] +pub struct SchemaClass { + class: Vec, + name: String, + description: String, + // This allows modification of system types to be extended in custom ways + systemmay: Vec, + may: Vec, + systemmust: Vec, + must: Vec, +} + +impl SchemaClass { + // Implement Validation and Normalisation against entries + pub fn validate_entry(&self, entry: &Entry) -> Result<(), ()> { + Err(()) + } +} + +#[derive(Debug)] pub struct Schema { // We contain sets of classes and attributes. classes: HashMap, @@ -45,26 +107,548 @@ pub struct Schema { impl Schema { pub fn new() -> Self { // - // Bootstrap in definitions of our own schema types - Schema { + let mut s = Schema { classes: HashMap::new(), attributes: HashMap::new(), + }; + // Bootstrap in definitions of our own schema types + // First, add all the needed core attributes for schema parsing + s.attributes.insert( + String::from("class"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("class"), + description: String::from("The set of classes defining an object"), + system: true, + secret: false, + multivalue: true, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }, + ); + s.attributes.insert( + String::from("name"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("name"), + description: String::from("The shortform name of an object"), + system: true, + secret: false, + multivalue: false, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }, + ); + s.attributes.insert( + String::from("description"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("description"), + description: String::from("A description of an attribute, object or class"), + system: true, + secret: false, + multivalue: false, + index: vec![], + syntax: SyntaxType::UTF8STRING, + }, + ); + s.attributes.insert( + String::from("system"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("system"), + description: String::from( + "Is this object or attribute provided from the core system?", + ), + system: true, + secret: false, + multivalue: false, + index: vec![], + syntax: SyntaxType::BOOLEAN, + }, + ); + s.attributes.insert(String::from("secret"), SchemaAttribute { + 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, + secret: false, + multivalue: false, + index: vec![], + syntax: SyntaxType::BOOLEAN, + }); + s.attributes.insert(String::from("multivalue"), SchemaAttribute { + 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, + secret: false, + multivalue: false, + index: vec![], + syntax: SyntaxType::BOOLEAN, + }); + s.attributes.insert( + String::from("index"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("index"), + description: String::from( + "Describe the indexes to apply to instances of this attribute.", + ), + system: true, + secret: false, + multivalue: false, + index: vec![], + syntax: SyntaxType::INDEX_ID, + }, + ); + s.attributes.insert( + String::from("syntax"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("syntax"), + description: String::from( + "Describe the syntax of this attribute. This affects indexing and sorting.", + ), + system: true, + secret: false, + multivalue: false, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::SYNTAX_ID, + }, + ); + s.attributes.insert( + String::from("systemmay"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("systemmay"), + description: String::from( + "A list of system provided optional attributes this class can store.", + ), + system: true, + secret: false, + multivalue: true, + index: vec![], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }, + ); + s.attributes.insert( + String::from("may"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("may"), + description: String::from( + "A user modifiable list of optional attributes this class can store.", + ), + system: true, + secret: false, + multivalue: false, + index: vec![], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }, + ); + s.attributes.insert( + String::from("systemmust"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("systemmust"), + description: String::from( + "A list of system provided required attributes this class must store.", + ), + system: true, + secret: false, + multivalue: false, + index: vec![], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }, + ); + s.attributes.insert( + String::from("must"), + SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("must"), + description: String::from( + "A user modifiable list of required attributes this class must store.", + ), + system: true, + secret: false, + multivalue: false, + index: vec![], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }, + ); + + s.classes.insert( + String::from("attributetype"), + SchemaClass { + 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")], + may: vec![], + systemmust: vec![ + String::from("class"), + String::from("name"), + String::from("system"), + String::from("secret"), + String::from("multivalue"), + String::from("syntax"), + ], + must: vec![], + }, + ); + s.classes.insert( + String::from("classtype"), + SchemaClass { + 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")], + must: vec![], + }, + ); + s.classes.insert( + String::from("extensibleobject"), + SchemaClass { + class: vec![String::from("classtype")], + name: String::from("extensibleobject"), + description: String::from("A class type that turns off all rules ..."), + systemmay: vec![], + may: vec![], + systemmust: vec![], + must: vec![], + }, + ); + + s + } + + pub fn validate(&self) -> Result<(), ()> { + // FIXME: How can we make this return a proper result? + // + // Do we need some functional bullshit? + // Validate our schema content is sane + // For now we only have a few basic methods for this, such as + // checking all our classes must/may are correct. + for class in self.classes.values() { + for a in &class.systemmay { + assert!(self.attributes.contains_key(a)); + } + for a in &class.may { + assert!(self.attributes.contains_key(a)); + } + for a in &class.systemmust { + assert!(self.attributes.contains_key(a)); + } + for a in &class.must { + assert!(self.attributes.contains_key(a)); + } } + + Ok(()) + } + + pub fn validate_entry(&self, entry: &Entry) -> Result<(), SchemaError> { + // First look at the classes on the entry. + // Now, check they are valid classes + // + // FIXME: We could restructure this to be a map that gets Some(class) + // if found, then do a len/filter/check on the resulting class set? + let c_valid = entry.classes().fold(Ternary::Empty, |acc, c| { + if acc == Ternary::False { + // Begin shortcircuit + acc + } else { + // Test the value (Could be True or Valid on entry. + // We + match self.classes.contains_key(c) { + true => Ternary::True, + false => Ternary::False, + } + } + }); + + if c_valid != Ternary::True { + return Err(SchemaError::INVALID_CLASS); + }; + + let classes: HashMap = entry + .classes() + .map(|c| (c.clone(), self.classes.get(c).unwrap())) + .collect(); + + let extensible = classes.contains_key("extensibleobject"); + + // What this is really doing is taking a set of classes, and building an + // "overall" class that describes this exact object for checking + + // for each class + // add systemmust/must and systemmay/may to their lists + // add anything from must also into may + + // Now from the set of valid classes make a list of must/may + // FIXME: This is clone on read, which may be really slow. It also may + // be inefficent on duplicates etc. + let must: HashMap = classes + .iter() + // Join our class systemmmust + must into one iter + .flat_map(|(_, cls)| cls.systemmust.iter().chain(cls.must.iter())) + .map(|s| (s.clone(), self.attributes.get(s).unwrap())) + .collect(); + + let may: HashMap = classes + .iter() + // Join our class systemmmust + must into one iter + .flat_map(|(_, cls)| { + cls.systemmust + .iter() + .chain(cls.must.iter()) + .chain(cls.systemmay.iter()) + .chain(cls.may.iter()) + }) + .map(|s| (s.clone(), self.attributes.get(s).unwrap())) + .collect(); + + // FIXME: Error needs to say what is missing + // We need to return *all* missing attributes. + + // Check that all must are inplace + // for each attr in must, check it's present on our ent + for (attr_name, attr) in must { + let avas = entry.get_ava(&attr_name); + if avas.is_none() { + return Err(SchemaError::MISSING_MUST_ATTRIBUTE); + } + } + + // 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. + } + None => { + if !extensible { + return Err(SchemaError::INVALID_ATTRIBUTE); + } + } + } + } + + // Well, we got here, so okay! + Ok(()) } } - #[cfg(test)] mod tests { - use super::{Schema, SchemaClass, SchemaAttribute}; + use std::convert::TryFrom; use super::super::entry::Entry; + use super::super::error::SchemaError; + use super::{IndexType, Schema, SchemaAttribute, SchemaClass, SyntaxType}; + + #[test] + fn test_schema_index_tryfrom() { + let r1 = IndexType::try_from(String::from("EQUALITY")); + assert_eq!(r1, Ok(IndexType::EQUALITY)); + + let r2 = IndexType::try_from(String::from("PRESENCE")); + assert_eq!(r2, Ok(IndexType::PRESENCE)); + + let r3 = IndexType::try_from(String::from("SUBSTRING")); + assert_eq!(r3, Ok(IndexType::SUBSTRING)); + + let r4 = IndexType::try_from(String::from("thaoeusaneuh")); + assert_eq!(r4, Err(())); + } + + #[test] + fn test_schema_syntax_tryfrom() { + + } #[test] fn test_schema_attribute_simple() { + let class_attribute = SchemaAttribute { + class: vec![String::from("attributetype")], + name: String::from("class"), + description: String::from("The set of classes defining an object"), + system: true, + secret: false, + multivalue: true, + index: vec![IndexType::EQUALITY], + syntax: SyntaxType::UTF8STRING_INSENSITIVE, + }; + // Test basic functions of simple attributes + } + + #[test] + fn test_schema_classes_simple() { // Test basic functions of simple attributes } #[test] fn test_schema_simple() { + let schema = Schema::new(); + assert!(schema.validate().is_ok()); + } + + #[test] + fn test_schema_export_validate() { + // Test exporting schema to entries, then validate them + // as legitimate entries. + } + + #[test] + fn test_schema_entries() { + // Given an entry, assert it's schema is valid + // We do + let schema = Schema::new(); + let mut e_no_class: Entry = Entry::new(); + 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(); + 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(); + + 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(); + + 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(); + + 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(); + + assert_eq!(schema.validate_entry(&e_ok), Ok(())); + } + + #[test] + 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(); + + 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(); + + /* 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 } }