//! [`Schema`] is one of the foundational concepts of the server. It provides a //! set of rules to enforce that [`Entries`] ava's must be compliant to, to be //! considered valid for commit to the database. This allows us to provide //! requirements and structure as to what an [`Entry`] must have and may contain //! which enables many other parts to function. //! //! To define this structure we define [`Attributes`] that provide rules for how //! and ava should be structured. We also define [`Classes`] that define //! the rules of which [`Attributes`] may or must exist on an [`Entry`] for it //! to be considered valid. An [`Entry`] must have at least 1 to infinite //! [`Classes`]. [`Classes'] are additive. //! //! [`Schema`]: struct.Schema.html //! [`Entries`]: ../entry/index.html //! [`Entry`]: ../entry/index.html //! [`Attributes`]: struct.SchemaAttribute.html //! [`Classes`]: struct.SchemaClass.html use crate::be::IdxKey; use crate::prelude::*; use crate::valueset::ValueSet; use kanidm_proto::v1::{ConsistencyError, OperationError, SchemaError}; use tracing::trace; use hashbrown::{HashMap, HashSet}; use uuid::Uuid; use concread::cowcell::*; // representations of schema that confines object types, classes // and attributes. This ties in deeply with "Entry". // // In the future this will parse/read it's schema from the db // but we have to bootstrap with some core types. // TODO #72: prefix on all schema types that are system? lazy_static! { static ref PVCLASS_ATTRIBUTETYPE: PartialValue = PartialValue::new_class("attributetype"); static ref PVCLASS_CLASSTYPE: PartialValue = PartialValue::new_class("classtype"); } /// Schema stores the set of [`Classes`] and [`Attributes`] that the server will /// use to validate [`Entries`], [`Filters`] and [`Modifications`]. Additionally the /// schema stores an extracted copy of the current attribute indexing metadata that /// is used by the backend during queries. /// /// [`Filters`]: ../filter/index.html /// [`Modifications`]: ../modify/index.html /// [`Entries`]: ../entry/index.html /// [`Attributes`]: struct.SchemaAttribute.html /// [`Classes`]: struct.SchemaClass.html pub struct Schema { classes: CowCell>, attributes: CowCell>, unique_cache: CowCell>, ref_cache: CowCell>, } /// A writable transaction of the working schema set. You should not change this directly, /// the writability is for the server internally to allow reloading of the schema. Changes /// you make will be lost when the server re-reads the schema from disk. pub struct SchemaWriteTransaction<'a> { classes: CowCellWriteTxn<'a, HashMap>, attributes: CowCellWriteTxn<'a, HashMap>, unique_cache: CowCellWriteTxn<'a, Vec>, ref_cache: CowCellWriteTxn<'a, HashMap>, } /// A readonly transaction of the working schema set. pub struct SchemaReadTransaction { classes: CowCellReadTxn>, attributes: CowCellReadTxn>, unique_cache: CowCellReadTxn>, ref_cache: CowCellReadTxn>, } /// An item reperesenting an attribute and the rules that enforce it. These rules enforce if an /// attribute on an [`Entry`] may be single or multi value, must be unique amongst all other types /// of this attribute, if the attribute should be [`indexed`], and what type of data [`syntax`] it may hold. /// /// [`Entry`]: ../entry/index.html /// [`indexed`]: ../value/enum.IndexType.html /// [`syntax`]: ../value/enum.SyntaxType.html #[derive(Debug, Clone)] pub struct SchemaAttribute { // Is this ... used? // class: Vec, pub name: AttrString, pub uuid: Uuid, // Perhaps later add aliases? pub description: String, pub multivalue: bool, pub unique: bool, pub phantom: bool, pub index: Vec, pub syntax: SyntaxType, } impl SchemaAttribute { pub fn try_from(value: &Entry) -> Result { // Convert entry to a schema attribute. trace!("Converting -> {:?}", value); // uuid let uuid = value.get_uuid(); // class if !value.attribute_equality("class", &PVCLASS_ATTRIBUTETYPE) { admin_error!("class attribute type not present - {:?}", uuid); return Err(OperationError::InvalidSchemaState( "missing attributetype".to_string(), )); } // name let name = value .get_ava_single_iutf8("attributename") .map(|s| s.into()) .ok_or_else(|| { admin_error!("missing attributename - {:?}", uuid); OperationError::InvalidSchemaState("missing attributename".to_string()) })?; // description let description = value .get_ava_single_utf8("description") .map(|s| s.to_string()) .ok_or_else(|| { admin_error!("missing description - {}", name); OperationError::InvalidSchemaState("missing description".to_string()) })?; // multivalue let multivalue = value.get_ava_single_bool("multivalue").ok_or_else(|| { admin_error!("missing multivalue - {}", name); OperationError::InvalidSchemaState("missing multivalue".to_string()) })?; let unique = value.get_ava_single_bool("unique").ok_or_else(|| { admin_error!("missing unique - {}", name); OperationError::InvalidSchemaState("missing unique".to_string()) })?; let phantom = value.get_ava_single_bool("phantom").unwrap_or(false); // index vec // even if empty, it SHOULD be present ... (is that valid to put an empty set?) // The get_ava_opt_index handles the optional case for us :) let index = value.get_ava_opt_index("index").ok_or_else(|| { admin_error!("invalid index - {}", name); OperationError::InvalidSchemaState("invalid index".to_string()) })?; // syntax type let syntax = value.get_ava_single_syntax("syntax").ok_or_else(|| { admin_error!("missing syntax - {}", name); OperationError::InvalidSchemaState("missing syntax".to_string()) })?; Ok(SchemaAttribute { name, uuid, description, multivalue, unique, phantom, index, syntax, }) } // There may be a difference between a value and a filter value on complex // types - IE a complex type may have multiple parts that are secret, but a filter // on that may only use a single tagged attribute for example. pub fn validate_partialvalue(&self, a: &str, v: &PartialValue) -> Result<(), SchemaError> { let r = match self.syntax { SyntaxType::Boolean => v.is_bool(), SyntaxType::SYNTAX_ID => v.is_syntax(), SyntaxType::INDEX_ID => v.is_index(), SyntaxType::Uuid => v.is_uuid(), SyntaxType::REFERENCE_UUID => v.is_refer(), SyntaxType::Utf8StringInsensitive => v.is_iutf8(), SyntaxType::Utf8StringIname => v.is_iname(), SyntaxType::UTF8STRING => v.is_utf8(), SyntaxType::JSON_FILTER => v.is_json_filter(), SyntaxType::Credential => v.is_credential(), SyntaxType::SecretUtf8String => v.is_secret_string(), SyntaxType::SshKey => v.is_sshkey(), SyntaxType::SecurityPrincipalName => v.is_spn(), SyntaxType::UINT32 => v.is_uint32(), SyntaxType::Cid => v.is_cid(), SyntaxType::NsUniqueId => v.is_nsuniqueid(), SyntaxType::DateTime => v.is_datetime(), SyntaxType::EmailAddress => v.is_email_address(), SyntaxType::Url => v.is_url(), SyntaxType::OauthScope => v.is_oauthscope(), SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(), SyntaxType::PrivateBinary => v.is_privatebinary(), SyntaxType::IntentToken => matches!(v, PartialValue::IntentToken(_)), }; if r { Ok(()) } else { trace!( ?a, ?self, ?v, "validate_partialvalue InvalidAttributeSyntax" ); Err(SchemaError::InvalidAttributeSyntax(a.to_string())) } } pub fn validate_value(&self, a: &str, v: &Value) -> Result<(), SchemaError> { let r = v.validate() && match self.syntax { SyntaxType::Boolean => v.is_bool(), SyntaxType::SYNTAX_ID => v.is_syntax(), SyntaxType::INDEX_ID => v.is_index(), SyntaxType::Uuid => v.is_uuid(), SyntaxType::REFERENCE_UUID => v.is_refer(), SyntaxType::Utf8StringInsensitive => v.is_iutf8(), SyntaxType::Utf8StringIname => v.is_iname(), SyntaxType::UTF8STRING => v.is_utf8(), SyntaxType::JSON_FILTER => v.is_json_filter(), SyntaxType::Credential => v.is_credential(), SyntaxType::SecretUtf8String => v.is_secret_string(), SyntaxType::SshKey => v.is_sshkey(), SyntaxType::SecurityPrincipalName => v.is_spn(), SyntaxType::UINT32 => v.is_uint32(), SyntaxType::Cid => v.is_cid(), SyntaxType::NsUniqueId => v.is_nsuniqueid(), SyntaxType::DateTime => v.is_datetime(), SyntaxType::EmailAddress => v.is_email_address(), SyntaxType::Url => v.is_url(), SyntaxType::OauthScope => v.is_oauthscope(), SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(), SyntaxType::PrivateBinary => v.is_privatebinary(), SyntaxType::IntentToken => matches!(v, Value::IntentToken(_, _)), }; if r { Ok(()) } else { trace!( ?a, ?self, ?v, "validate_value failure - InvalidAttributeSyntax" ); Err(SchemaError::InvalidAttributeSyntax(a.to_string())) } } pub fn validate_ava(&self, a: &str, ava: &ValueSet) -> Result<(), SchemaError> { trace!("Checking for valid {:?} -> {:?}", self.name, ava); // Check multivalue if !self.multivalue && ava.len() > 1 { // lrequest_error!("Ava len > 1 on single value attribute!"); admin_error!("Ava len > 1 on single value attribute!"); return Err(SchemaError::InvalidAttributeSyntax(a.to_string())); }; // If syntax, check the type is correct let valid = self.syntax == ava.syntax(); if valid && ava.validate(self) { Ok(()) } else { admin_error!( ?a, "validate_ava - InvalidAttributeSyntax for {:?}", self.syntax ); Err(SchemaError::InvalidAttributeSyntax(a.to_string())) } } } /// An item reperesenting a class and the rules for that class. These rules enforce that an /// [`Entry`]'s avas conform to a set of requirements, giving structure to an entry about /// what avas must or may exist. The kanidm project provides attributes in `systemmust` and /// `systemmay`, which can not be altered. An administrator may extend these in the `must` /// and `may` attributes. /// /// Classes are additive, meaning that if there are two classes, the `may` rules of both union, /// and that if an attribute is `must` on one class, and `may` in another, the `must` rule /// takes precedence. It is not possible to combine classes in an incompatible way due to these /// rules. /// /// That in mind, and entry that has one of every possible class would probably be nonsensical, /// but the addition rules make it easy to construct and understand with concepts like [`access`] /// controls or accounts and posix extensions. /// /// [`Entry`]: ../entry/index.html /// [`access`]: ../access/index.html #[derive(Debug, Clone)] pub struct SchemaClass { // Is this used? // class: Vec, pub name: AttrString, pub uuid: Uuid, pub description: String, // This allows modification of system types to be extended in custom ways pub systemmay: Vec, pub may: Vec, pub systemmust: Vec, pub must: Vec, } impl SchemaClass { pub fn try_from(value: &Entry) -> Result { trace!("Converting {:?}", value); // uuid let uuid = value.get_uuid(); // Convert entry to a schema class. if !value.attribute_equality("class", &PVCLASS_CLASSTYPE) { admin_error!("class classtype not present - {:?}", uuid); return Err(OperationError::InvalidSchemaState( "missing classtype".to_string(), )); } // name let name = value .get_ava_single_iutf8("classname") .map(AttrString::from) .ok_or_else(|| { admin_error!("missing classname - {:?}", uuid); OperationError::InvalidSchemaState("missing classname".to_string()) })?; // description let description = value .get_ava_single_utf8("description") .map(String::from) .ok_or_else(|| { admin_error!("missing description - {}", name); OperationError::InvalidSchemaState("missing description".to_string()) })?; // These are all "optional" lists of strings. let systemmay = value .get_ava_iter_iutf8("systemmay") .map(|i| i.map(AttrString::from).collect()) .unwrap_or_else(Vec::new); let systemmust = value .get_ava_iter_iutf8("systemmust") .map(|i| i.map(AttrString::from).collect()) .unwrap_or_else(Vec::new); let may = value .get_ava_iter_iutf8("may") .map(|i| i.map(AttrString::from).collect()) .unwrap_or_else(Vec::new); let must = value .get_ava_iter_iutf8("must") .map(|i| i.map(AttrString::from).collect()) .unwrap_or_else(Vec::new); Ok(SchemaClass { name, uuid, description, systemmay, may, systemmust, must, }) } } pub trait SchemaTransaction { fn get_classes(&self) -> &HashMap; fn get_attributes(&self) -> &HashMap; fn get_attributes_unique(&self) -> &Vec; fn get_reference_types(&self) -> &HashMap; fn validate(&self) -> Vec> { let mut res = Vec::new(); let class_snapshot = self.get_classes(); let attribute_snapshot = self.get_attributes(); // We need to check that every uuid is unique because during tests we aren't doing // a disk reload, which means we were missing this and causing potential migration // failures on upgrade. let mut unique_uuid_set = HashSet::new(); class_snapshot .values() .map(|class| &class.uuid) .chain(attribute_snapshot.values().map(|attr| &attr.uuid)) .for_each(|uuid| { // If the set did not have this value present, true is returned. if !unique_uuid_set.insert(uuid) { res.push(Err(ConsistencyError::SchemaUuidNotUnique(*uuid))) } }); class_snapshot.values().for_each(|class| { // report the class we are checking class .systemmay .iter() .chain(class.may.iter()) .chain(class.systemmust.iter()) .chain(class.must.iter()) .for_each(|a| { match attribute_snapshot.get(a) { Some(attr) => { // We have the attribute, ensure it's not a phantom. if attr.phantom { res.push(Err(ConsistencyError::SchemaClassPhantomAttribute( class.name.to_string(), a.to_string(), ))) } } None => { // No such attr, something is missing! res.push(Err(ConsistencyError::SchemaClassMissingAttribute( class.name.to_string(), a.to_string(), ))) } } }) }); // end for res } fn is_multivalue(&self, attr: &str) -> Result { match self.get_attributes().get(attr) { Some(a_schema) => Ok(a_schema.multivalue), None => { // ladmin_error!("Attribute does not exist?!"); Err(SchemaError::InvalidAttribute(attr.to_string())) } } } fn normalise_attr_name(&self, an: &str) -> AttrString { // Will duplicate. AttrString::from(an.to_lowercase()) } fn normalise_attr_if_exists(&self, an: &str) -> Option { if self.get_attributes().contains_key(an) { Some(self.normalise_attr_name(an)) } else { None } } } impl<'a> SchemaWriteTransaction<'a> { // Schema probably needs to be part of the backend, so that commits are wholly atomic // but in the current design, we need to open be first, then schema, but we have to commit be // first, then schema to ensure that the be content matches our schema. Saying this, if your // schema commit fails we need to roll back still .... How great are transactions. // At the least, this is what validation is for! pub fn commit(self) -> Result<(), OperationError> { let SchemaWriteTransaction { classes, attributes, unique_cache, ref_cache, } = self; unique_cache.commit(); ref_cache.commit(); classes.commit(); attributes.commit(); Ok(()) } pub fn update_attributes( &mut self, attributetypes: Vec, ) -> Result<(), OperationError> { // purge all old attributes. self.attributes.clear(); self.unique_cache.clear(); self.ref_cache.clear(); // Update with new ones. // Do we need to check for dups? // No, they'll over-write each other ... but we do need name uniqueness. attributetypes.into_iter().for_each(|a| { // Update the unique and ref caches. if a.syntax == SyntaxType::REFERENCE_UUID || a.syntax == SyntaxType::OauthScopeMap { self.ref_cache.insert(a.name.clone(), a.clone()); } if a.unique { self.unique_cache.push(a.name.clone()); } // Finally insert. self.attributes.insert(a.name.clone(), a); }); Ok(()) } pub fn update_classes(&mut self, classtypes: Vec) -> Result<(), OperationError> { // purge all old attributes. self.classes.clear(); // Update with new ones. // Do we need to check for dups? // No, they'll over-write each other ... but we do need name uniqueness. classtypes.into_iter().for_each(|a| { self.classes.insert(a.name.clone(), a); }); Ok(()) } pub fn to_entries(&self) -> Vec> { let r: Vec<_> = self .attributes .values() .map(Entry::::from) .chain( self.classes .values() .map(Entry::::from), ) .collect(); r } pub fn reload_idxmeta(&self) -> Vec { self.get_attributes() .values() .flat_map(|a| { a.index.iter().map(move |itype: &IndexType| IdxKey { attr: a.name.clone(), itype: (*itype).clone(), }) }) .collect() } pub fn generate_in_memory(&mut self) -> Result<(), OperationError> { spanned!("schema::generate_in_memory", { // self.classes.clear(); self.attributes.clear(); // Bootstrap in definitions of our own schema types // First, add all the needed core attributes for schema parsing self.attributes.insert( AttrString::from("class"), SchemaAttribute { name: AttrString::from("class"), uuid: UUID_SCHEMA_ATTR_CLASS, description: String::from("The set of classes defining an object"), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality, IndexType::Presence], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("uuid"), SchemaAttribute { name: AttrString::from("uuid"), uuid: UUID_SCHEMA_ATTR_UUID, description: String::from("The universal unique id of the object"), multivalue: false, // Uniqueness is handled by base.rs, not attrunique here due to // needing to check recycled objects too. unique: false, phantom: false, index: vec![IndexType::Equality, IndexType::Presence], syntax: SyntaxType::Uuid, }, ); self.attributes.insert( AttrString::from("last_modified_cid"), SchemaAttribute { name: AttrString::from("last_modified_cid"), uuid: UUID_SCHEMA_ATTR_LAST_MOD_CID, description: String::from("The cid of the last change to this object"), multivalue: false, // Uniqueness is handled by base.rs, not attrunique here due to // needing to check recycled objects too. unique: false, phantom: false, index: vec![], syntax: SyntaxType::Cid, }, ); self.attributes.insert( AttrString::from("name"), SchemaAttribute { name: AttrString::from("name"), uuid: UUID_SCHEMA_ATTR_NAME, description: String::from("The shortform name of an object"), multivalue: false, unique: true, phantom: false, index: vec![IndexType::Equality, IndexType::Presence], syntax: SyntaxType::Utf8StringIname, }, ); self.attributes.insert( AttrString::from("spn"), SchemaAttribute { name: AttrString::from("spn"), uuid: UUID_SCHEMA_ATTR_SPN, description: String::from( "The Security Principal Name of an object, unique across all domain trusts", ), multivalue: false, unique: true, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::SecurityPrincipalName, }, ); self.attributes.insert( AttrString::from("attributename"), SchemaAttribute { name: AttrString::from("attributename"), uuid: UUID_SCHEMA_ATTR_ATTRIBUTENAME, description: String::from("The name of a schema attribute"), multivalue: false, unique: true, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("classname"), SchemaAttribute { name: AttrString::from("classname"), uuid: UUID_SCHEMA_ATTR_CLASSNAME, description: String::from("The name of a schema class"), multivalue: false, unique: true, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("description"), SchemaAttribute { name: AttrString::from("description"), uuid: UUID_SCHEMA_ATTR_DESCRIPTION, description: String::from("A description of an attribute, object or class"), multivalue: false, unique: false, phantom: false, index: vec![], syntax: SyntaxType::UTF8STRING, }, ); self.attributes.insert(AttrString::from("multivalue"), SchemaAttribute { name: AttrString::from("multivalue"), uuid: UUID_SCHEMA_ATTR_MULTIVALUE, description: String::from("If true, this attribute is able to store multiple values rather than just a single value."), multivalue: false, unique: false, phantom: false, index: vec![], syntax: SyntaxType::Boolean, }); self.attributes.insert(AttrString::from("phantom"), SchemaAttribute { name: AttrString::from("phantom"), uuid: UUID_SCHEMA_ATTR_PHANTOM, description: String::from("If true, this attribute must NOT be present in any may/must sets of a class as. This represents generated attributes."), multivalue: false, unique: false, phantom: false, index: vec![], syntax: SyntaxType::Boolean, }); self.attributes.insert(AttrString::from("unique"), SchemaAttribute { name: AttrString::from("unique"), uuid: UUID_SCHEMA_ATTR_UNIQUE, description: String::from("If true, this attribute must store a unique value through out the database."), multivalue: false, unique: false, phantom: false, index: vec![], syntax: SyntaxType::Boolean, }); self.attributes.insert( AttrString::from("index"), SchemaAttribute { name: AttrString::from("index"), uuid: UUID_SCHEMA_ATTR_INDEX, description: String::from( "Describe the indexes to apply to instances of this attribute.", ), multivalue: true, unique: false, phantom: false, index: vec![], syntax: SyntaxType::INDEX_ID, }, ); self.attributes.insert( AttrString::from("syntax"), SchemaAttribute { name: AttrString::from("syntax"), uuid: UUID_SCHEMA_ATTR_SYNTAX, description: String::from( "Describe the syntax of this attribute. This affects indexing and sorting.", ), multivalue: false, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::SYNTAX_ID, }, ); self.attributes.insert( AttrString::from("systemmay"), SchemaAttribute { name: AttrString::from("systemmay"), uuid: UUID_SCHEMA_ATTR_SYSTEMMAY, description: String::from( "A list of system provided optional attributes this class can store.", ), multivalue: true, unique: false, phantom: false, index: vec![], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("may"), SchemaAttribute { name: AttrString::from("may"), uuid: UUID_SCHEMA_ATTR_MAY, description: String::from( "A user modifiable list of optional attributes this class can store.", ), multivalue: true, unique: false, phantom: false, index: vec![], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("systemmust"), SchemaAttribute { name: AttrString::from("systemmust"), uuid: UUID_SCHEMA_ATTR_SYSTEMMUST, description: String::from( "A list of system provided required attributes this class must store.", ), multivalue: true, unique: false, phantom: false, index: vec![], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("must"), SchemaAttribute { name: AttrString::from("must"), uuid: UUID_SCHEMA_ATTR_MUST, description: String::from( "A user modifiable list of required attributes this class must store.", ), multivalue: true, unique: false, phantom: false, index: vec![], syntax: SyntaxType::Utf8StringInsensitive, }, ); // SYSINFO attrs // ACP attributes. self.attributes.insert( AttrString::from("acp_enable"), SchemaAttribute { name: AttrString::from("acp_enable"), uuid: UUID_SCHEMA_ATTR_ACP_ENABLE, description: String::from("A flag to determine if this ACP is active for application. True is enabled, and enforce. False is checked but not enforced."), multivalue: false, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Boolean, }, ); self.attributes.insert( AttrString::from("acp_receiver"), SchemaAttribute { name: AttrString::from("acp_receiver"), uuid: UUID_SCHEMA_ATTR_ACP_RECEIVER, description: String::from( "Who the ACP applies to, constraining or allowing operations.", ), multivalue: false, unique: false, phantom: false, index: vec![IndexType::Equality, IndexType::SubString], syntax: SyntaxType::JSON_FILTER, }, ); self.attributes.insert( AttrString::from("acp_targetscope"), SchemaAttribute { name: AttrString::from("acp_targetscope"), uuid: UUID_SCHEMA_ATTR_ACP_TARGETSCOPE, description: String::from( "The effective targets of the ACP, IE what will be acted upon.", ), multivalue: false, unique: false, phantom: false, index: vec![IndexType::Equality, IndexType::SubString], syntax: SyntaxType::JSON_FILTER, }, ); self.attributes.insert( AttrString::from("acp_search_attr"), SchemaAttribute { name: AttrString::from("acp_search_attr"), uuid: UUID_SCHEMA_ATTR_ACP_SEARCH_ATTR, description: String::from("The attributes that may be viewed or searched by the reciever on targetscope."), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("acp_create_class"), SchemaAttribute { name: AttrString::from("acp_create_class"), uuid: UUID_SCHEMA_ATTR_ACP_CREATE_CLASS, description: String::from( "The set of classes that can be created on a new entry.", ), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("acp_create_attr"), SchemaAttribute { name: AttrString::from("acp_create_attr"), uuid: UUID_SCHEMA_ATTR_ACP_CREATE_ATTR, description: String::from( "The set of attribute types that can be created on an entry.", ), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("acp_modify_removedattr"), SchemaAttribute { name: AttrString::from("acp_modify_removedattr"), uuid: UUID_SCHEMA_ATTR_ACP_MODIFY_REMOVEDATTR, description: String::from("The set of attribute types that could be removed or purged in a modification."), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("acp_modify_presentattr"), SchemaAttribute { name: AttrString::from("acp_modify_presentattr"), uuid: UUID_SCHEMA_ATTR_ACP_MODIFY_PRESENTATTR, description: String::from("The set of attribute types that could be added or asserted in a modification."), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("acp_modify_class"), SchemaAttribute { name: AttrString::from("acp_modify_class"), uuid: UUID_SCHEMA_ATTR_ACP_MODIFY_CLASS, description: String::from("The set of class values that could be asserted or added to an entry. Only applies to modify::present operations on class."), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringInsensitive, }, ); // MO/Member self.attributes.insert( AttrString::from("memberof"), SchemaAttribute { name: AttrString::from("memberof"), uuid: UUID_SCHEMA_ATTR_MEMBEROF, description: String::from("reverse group membership of the object"), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::REFERENCE_UUID, }, ); self.attributes.insert( AttrString::from("directmemberof"), SchemaAttribute { name: AttrString::from("directmemberof"), uuid: UUID_SCHEMA_ATTR_DIRECTMEMBEROF, description: String::from("reverse direct group membership of the object"), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::REFERENCE_UUID, }, ); self.attributes.insert( AttrString::from("member"), SchemaAttribute { name: AttrString::from("member"), uuid: UUID_SCHEMA_ATTR_MEMBER, description: String::from("List of members of the group"), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::REFERENCE_UUID, }, ); // Migration related self.attributes.insert( AttrString::from("version"), SchemaAttribute { name: AttrString::from("version"), uuid: UUID_SCHEMA_ATTR_VERSION, description: String::from( "The systems internal migration version for provided objects", ), multivalue: false, unique: false, phantom: false, index: vec![], syntax: SyntaxType::UINT32, }, ); // Domain for sysinfo self.attributes.insert( AttrString::from("domain"), SchemaAttribute { name: AttrString::from("domain"), uuid: UUID_SCHEMA_ATTR_DOMAIN, description: String::from("A DNS Domain name entry."), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringIname, }, ); self.attributes.insert( AttrString::from("claim"), SchemaAttribute { name: AttrString::from("claim"), uuid: UUID_SCHEMA_ATTR_CLAIM, description: String::from( "The string identifier of an extracted claim that can be filtered", ), multivalue: true, unique: false, phantom: true, index: vec![], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("password_import"), SchemaAttribute { name: AttrString::from("password_import"), uuid: UUID_SCHEMA_ATTR_PASSWORD_IMPORT, description: String::from("An imported password hash from an external system."), multivalue: true, unique: false, phantom: true, index: vec![], syntax: SyntaxType::UTF8STRING, }, ); // LDAP Masking Phantoms self.attributes.insert( AttrString::from("dn"), SchemaAttribute { name: AttrString::from("dn"), uuid: UUID_SCHEMA_ATTR_DN, description: String::from("An LDAP Compatible DN"), multivalue: false, unique: false, phantom: true, index: vec![], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("entrydn"), SchemaAttribute { name: AttrString::from("entrydn"), uuid: UUID_SCHEMA_ATTR_ENTRYDN, description: String::from("An LDAP Compatible EntryDN"), multivalue: false, unique: false, phantom: true, index: vec![], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("entryuuid"), SchemaAttribute { name: AttrString::from("entryuuid"), uuid: UUID_SCHEMA_ATTR_ENTRYUUID, description: String::from("An LDAP Compatible entryUUID"), multivalue: false, unique: false, phantom: true, index: vec![], syntax: SyntaxType::Uuid, }, ); self.attributes.insert( AttrString::from("objectclass"), SchemaAttribute { name: AttrString::from("objectclass"), uuid: UUID_SCHEMA_ATTR_OBJECTCLASS, description: String::from("An LDAP Compatible objectClass"), multivalue: true, unique: false, phantom: true, index: vec![], syntax: SyntaxType::Utf8StringInsensitive, }, ); self.attributes.insert( AttrString::from("cn"), SchemaAttribute { name: AttrString::from("cn"), uuid: UUID_SCHEMA_ATTR_CN, description: String::from("An LDAP Compatible objectClass"), multivalue: false, unique: false, phantom: true, index: vec![], syntax: SyntaxType::Utf8StringIname, }, ); self.attributes.insert( AttrString::from("keys"), SchemaAttribute { name: AttrString::from("keys"), uuid: UUID_SCHEMA_ATTR_KEYS, description: String::from("An LDAP Compatible keys (ssh)"), multivalue: true, unique: false, phantom: true, index: vec![], syntax: SyntaxType::SshKey, }, ); self.attributes.insert( AttrString::from("sshpublickey"), SchemaAttribute { name: AttrString::from("sshpublickey"), uuid: UUID_SCHEMA_ATTR_SSHPUBLICKEY, description: String::from("An LDAP Compatible sshPublicKey"), multivalue: true, unique: false, phantom: true, index: vec![], syntax: SyntaxType::SshKey, }, ); self.attributes.insert( AttrString::from("email"), SchemaAttribute { name: AttrString::from("email"), uuid: UUID_SCHEMA_ATTR_EMAIL, description: String::from("An LDAP Compatible email"), multivalue: true, unique: false, phantom: true, index: vec![], syntax: SyntaxType::EmailAddress, }, ); self.attributes.insert( AttrString::from("emailaddress"), SchemaAttribute { name: AttrString::from("emailaddress"), uuid: UUID_SCHEMA_ATTR_EMAILADDRESS, description: String::from("An LDAP Compatible emailAddress"), multivalue: true, unique: false, phantom: true, index: vec![], syntax: SyntaxType::EmailAddress, }, ); self.attributes.insert( AttrString::from("uidnumber"), SchemaAttribute { name: AttrString::from("uidnumber"), uuid: UUID_SCHEMA_ATTR_UIDNUMBER, description: String::from("An LDAP Compatible uidNumber"), multivalue: false, unique: false, phantom: true, index: vec![], syntax: SyntaxType::UINT32, }, ); // end LDAP masking phantoms self.classes.insert( AttrString::from("attributetype"), SchemaClass { name: AttrString::from("attributetype"), uuid: UUID_SCHEMA_CLASS_ATTRIBUTETYPE, description: String::from("Definition of a schema attribute"), systemmay: vec![AttrString::from("phantom"), AttrString::from("index")], may: vec![], systemmust: vec![ AttrString::from("class"), AttrString::from("attributename"), AttrString::from("multivalue"), AttrString::from("unique"), AttrString::from("syntax"), AttrString::from("description"), ], must: vec![], }, ); self.classes.insert( AttrString::from("classtype"), SchemaClass { name: AttrString::from("classtype"), uuid: UUID_SCHEMA_CLASS_CLASSTYPE, description: String::from("Definition of a schema classtype"), systemmay: vec![ AttrString::from("systemmay"), AttrString::from("may"), AttrString::from("systemmust"), AttrString::from("must"), ], may: vec![], systemmust: vec![ AttrString::from("class"), AttrString::from("classname"), AttrString::from("description"), ], must: vec![], }, ); self.classes.insert( AttrString::from("object"), SchemaClass { name: AttrString::from("object"), uuid: UUID_SCHEMA_CLASS_OBJECT, description: String::from( "A system created class that all objects must contain", ), systemmay: vec![AttrString::from("description")], may: vec![], systemmust: vec![ AttrString::from("class"), AttrString::from("uuid"), AttrString::from("last_modified_cid"), ], must: vec![], }, ); self.classes.insert( AttrString::from("memberof"), SchemaClass { name: AttrString::from("memberof"), uuid: UUID_SCHEMA_CLASS_MEMBEROF, description: String::from("Class that is dynamically added to recepients of memberof or directmemberof"), systemmay: vec![ AttrString::from("memberof"), AttrString::from("directmemberof") ], may: vec![], systemmust: vec![], must: vec![], }, ); self.classes.insert( AttrString::from("extensibleobject"), SchemaClass { name: AttrString::from("extensibleobject"), uuid: UUID_SCHEMA_CLASS_EXTENSIBLEOBJECT, description: String::from( "A class type that has green hair and turns off all rules ...", ), systemmay: vec![], may: vec![], systemmust: vec![], must: vec![], }, ); /* These two classes are core to the entry lifecycle for recycling and tombstoning */ self.classes.insert( AttrString::from("recycled"), SchemaClass { name: AttrString::from("recycled"), uuid: UUID_SCHEMA_CLASS_RECYCLED, description: String::from("An object that has been deleted, but still recoverable via the revive operation. Recycled objects are not modifiable, only revivable."), systemmay: vec![], may: vec![], systemmust: vec![], must: vec![], }, ); self.classes.insert( AttrString::from("tombstone"), SchemaClass { name: AttrString::from("tombstone"), uuid: UUID_SCHEMA_CLASS_TOMBSTONE, description: String::from("An object that is purged from the recycle bin. This is a system internal state. Tombstones have no attributes beside UUID."), systemmay: vec![], may: vec![], systemmust: vec![ AttrString::from("class"), AttrString::from("uuid"), ], must: vec![], }, ); // sysinfo self.classes.insert( AttrString::from("system_info"), SchemaClass { name: AttrString::from("system_info"), uuid: UUID_SCHEMA_CLASS_SYSTEM_INFO, description: String::from("System metadata object class"), systemmay: vec![], may: vec![], systemmust: vec![ AttrString::from("version"), // Needed when we implement principalnames? // String::from("domain"), // String::from("hostname"), ], must: vec![], }, ); // ACP self.classes.insert( AttrString::from("access_control_profile"), SchemaClass { name: AttrString::from("access_control_profile"), uuid: UUID_SCHEMA_CLASS_ACCESS_CONTROL_PROFILE, description: String::from("System Access Control Profile Class"), systemmay: vec![ AttrString::from("acp_enable"), AttrString::from("description"), ], may: vec![], systemmust: vec![ AttrString::from("acp_receiver"), AttrString::from("acp_targetscope"), AttrString::from("name"), ], must: vec![], }, ); self.classes.insert( AttrString::from("access_control_search"), SchemaClass { name: AttrString::from("access_control_search"), uuid: UUID_SCHEMA_CLASS_ACCESS_CONTROL_SEARCH, description: String::from("System Access Control Search Class"), systemmay: vec![], may: vec![], systemmust: vec![AttrString::from("acp_search_attr")], must: vec![], }, ); self.classes.insert( AttrString::from("access_control_delete"), SchemaClass { name: AttrString::from("access_control_delete"), uuid: UUID_SCHEMA_CLASS_ACCESS_CONTROL_DELETE, description: String::from("System Access Control DELETE Class"), systemmay: vec![], may: vec![], systemmust: vec![], must: vec![], }, ); self.classes.insert( AttrString::from("access_control_modify"), SchemaClass { name: AttrString::from("access_control_modify"), uuid: UUID_SCHEMA_CLASS_ACCESS_CONTROL_MODIFY, description: String::from("System Access Control Modify Class"), systemmay: vec![ AttrString::from("acp_modify_removedattr"), AttrString::from("acp_modify_presentattr"), AttrString::from("acp_modify_class"), ], may: vec![], systemmust: vec![], must: vec![], }, ); self.classes.insert( AttrString::from("access_control_create"), SchemaClass { name: AttrString::from("access_control_create"), uuid: UUID_SCHEMA_CLASS_ACCESS_CONTROL_CREATE, description: String::from("System Access Control Create Class"), systemmay: vec![ AttrString::from("acp_create_class"), AttrString::from("acp_create_attr"), ], may: vec![], systemmust: vec![], must: vec![], }, ); self.classes.insert( AttrString::from("system"), SchemaClass { name: AttrString::from("system"), uuid: UUID_SCHEMA_CLASS_SYSTEM, description: String::from("A class denoting that a type is system generated and protected. It has special internal behaviour."), systemmay: vec![], may: vec![], systemmust: vec![], must: vec![], }, ); let r = self.validate(); if r.is_empty() { admin_info!("schema validate -> passed"); Ok(()) } else { admin_info!(err = ?r, "schema validate -> errors"); Err(OperationError::ConsistencyError(r)) } }) } } impl<'a> SchemaTransaction for SchemaWriteTransaction<'a> { fn get_attributes_unique(&self) -> &Vec { &(*self.unique_cache) } fn get_reference_types(&self) -> &HashMap { &(*self.ref_cache) } fn get_classes(&self) -> &HashMap { &(*self.classes) } fn get_attributes(&self) -> &HashMap { &(*self.attributes) } } impl SchemaTransaction for SchemaReadTransaction { fn get_attributes_unique(&self) -> &Vec { &(*self.unique_cache) } fn get_reference_types(&self) -> &HashMap { &(*self.ref_cache) } fn get_classes(&self) -> &HashMap { &(*self.classes) } fn get_attributes(&self) -> &HashMap { &(*self.attributes) } } impl Schema { pub fn new() -> Result { let s = Schema { classes: CowCell::new(HashMap::with_capacity(128)), attributes: CowCell::new(HashMap::with_capacity(128)), unique_cache: CowCell::new(Vec::new()), ref_cache: CowCell::new(HashMap::with_capacity(64)), }; // let mut sw = task::block_on(s.write()); let mut sw = s.write(); let r1 = sw.generate_in_memory(); debug_assert!(r1.is_ok()); r1?; let r2 = sw.commit().map(|_| s); debug_assert!(r2.is_ok()); r2 } pub fn read(&self) -> SchemaReadTransaction { SchemaReadTransaction { classes: self.classes.read(), attributes: self.attributes.read(), unique_cache: self.unique_cache.read(), ref_cache: self.ref_cache.read(), } } pub fn write(&self) -> SchemaWriteTransaction<'_> { SchemaWriteTransaction { classes: self.classes.write(), attributes: self.attributes.write(), unique_cache: self.unique_cache.write(), ref_cache: self.ref_cache.write(), } } pub(crate) fn write_blocking(&self) -> SchemaWriteTransaction<'_> { self.write() } /* pub async fn write<'a>(&'a self) -> SchemaWriteTransaction<'a> { SchemaWriteTransaction { classes: self.classes.write().await, attributes: self.attributes.write().await, unique_cache: self.unique_cache.write().await, ref_cache: self.ref_cache.write().await, } } #[cfg(test)] pub fn write_blocking<'a>(&'a self) -> SchemaWriteTransaction<'a> { task::block_on(self.write()) } */ } #[cfg(test)] mod tests { use crate::prelude::*; use crate::schema::SchemaTransaction; use crate::schema::{IndexType, Schema, SchemaAttribute, SchemaClass, SyntaxType}; use kanidm_proto::v1::{ConsistencyError, SchemaError}; use uuid::Uuid; // use crate::proto_v1::Filter as ProtoFilter; macro_rules! validate_schema { ($sch:ident) => {{ // Turns into a result type let r: Result, ConsistencyError> = $sch.validate().into_iter().collect(); assert!(r.is_ok()); }}; } macro_rules! sch_from_entry_ok { ( $e:expr, $type:ty ) => {{ let e1: Entry = Entry::unsafe_from_entry_str($e); let ev1 = unsafe { e1.into_sealed_committed() }; let r1 = <$type>::try_from(&ev1); assert!(r1.is_ok()); }}; } macro_rules! sch_from_entry_err { ( $e:expr, $type:ty ) => {{ let e1: Entry = Entry::unsafe_from_entry_str($e); let ev1 = unsafe { e1.into_sealed_committed() }; let r1 = <$type>::try_from(&ev1); assert!(r1.is_err()); }}; } #[test] fn test_schema_attribute_from_entry() { run_test!(|_qs: &QueryServer| { sch_from_entry_err!( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["schema_attr_test"], "unique": ["false"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"] } }"#, SchemaAttribute ); sch_from_entry_err!( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["schema_attr_test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "multivalue": ["false"], "unique": ["false"], "index": ["EQUALITY"], "syntax": ["UTF8STRING"] } }"#, SchemaAttribute ); sch_from_entry_err!( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["schema_attr_test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "description": ["Test attr parsing"], "multivalue": ["htouaoeu"], "unique": ["false"], "index": ["EQUALITY"], "syntax": ["UTF8STRING"] } }"#, SchemaAttribute ); sch_from_entry_err!( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["schema_attr_test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "description": ["Test attr parsing"], "multivalue": ["false"], "unique": ["false"], "index": ["NTEHNOU"], "syntax": ["UTF8STRING"] } }"#, SchemaAttribute ); sch_from_entry_err!( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["schema_attr_test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "description": ["Test attr parsing"], "multivalue": ["false"], "unique": ["false"], "index": ["EQUALITY"], "syntax": ["TNEOUNTUH"] } }"#, SchemaAttribute ); // Index is allowed to be empty sch_from_entry_ok!( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["schema_attr_test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "description": ["Test attr parsing"], "multivalue": ["false"], "unique": ["false"], "syntax": ["UTF8STRING"] } }"#, SchemaAttribute ); // Index present sch_from_entry_ok!( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["schema_attr_test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "description": ["Test attr parsing"], "multivalue": ["false"], "unique": ["false"], "index": ["EQUALITY"], "syntax": ["UTF8STRING"] } }"#, SchemaAttribute ); }); } #[test] fn test_schema_class_from_entry() { run_test!(|_qs: &QueryServer| { sch_from_entry_err!( r#"{ "attrs": { "class": ["object", "classtype"], "classname": ["schema_class_test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"] } }"#, SchemaClass ); sch_from_entry_err!( r#"{ "attrs": { "class": ["object"], "classname": ["schema_class_test"], "description": ["class test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"] } }"#, SchemaClass ); // Classes can be valid with no attributes provided. sch_from_entry_ok!( r#"{ "attrs": { "class": ["object", "classtype"], "classname": ["schema_class_test"], "description": ["class test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"] } }"#, SchemaClass ); // Classes with various may/must sch_from_entry_ok!( r#"{ "attrs": { "class": ["object", "classtype"], "classname": ["schema_class_test"], "description": ["class test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "systemmust": ["d"] } }"#, SchemaClass ); sch_from_entry_ok!( r#"{ "attrs": { "class": ["object", "classtype"], "classname": ["schema_class_test"], "description": ["class test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "systemmay": ["c"] } }"#, SchemaClass ); sch_from_entry_ok!( r#"{ "attrs": { "class": ["object", "classtype"], "classname": ["schema_class_test"], "description": ["class test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "may": ["a"], "must": ["b"] } }"#, SchemaClass ); sch_from_entry_ok!( r#"{ "attrs": { "class": ["object", "classtype"], "classname": ["schema_class_test"], "description": ["class test"], "uuid": ["66c68b2f-d02c-4243-8013-7946e40fe321"], "may": ["a"], "must": ["b"], "systemmay": ["c"], "systemmust": ["d"] } }"#, SchemaClass ); }); } #[test] fn test_schema_attribute_simple() { // Test schemaAttribute validation of types. // Test single value string let single_value_string = SchemaAttribute { // class: vec![String::from("attributetype")], name: AttrString::from("single_value"), uuid: Uuid::new_v4(), description: String::from(""), multivalue: false, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Utf8StringInsensitive, }; let r1 = single_value_string.validate_ava("single_value", &(vs_iutf8!["test"] as _)); assert_eq!(r1, Ok(())); let rvs = vs_iutf8!["test1", "test2"] as _; let r2 = single_value_string.validate_ava("single_value", &rvs); assert_eq!( r2, Err(SchemaError::InvalidAttributeSyntax( "single_value".to_string() )) ); // test multivalue string, boolean let multi_value_string = SchemaAttribute { // class: vec![String::from("attributetype")], name: AttrString::from("mv_string"), uuid: Uuid::new_v4(), description: String::from(""), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::UTF8STRING, }; let rvs = vs_utf8!["test1".to_string(), "test2".to_string()] as _; let r5 = multi_value_string.validate_ava("mv_string", &rvs); assert_eq!(r5, Ok(())); let multi_value_boolean = SchemaAttribute { // class: vec![String::from("attributetype")], name: AttrString::from("mv_bool"), uuid: Uuid::new_v4(), description: String::from(""), multivalue: true, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::Boolean, }; // Since valueset now disallows such shenangians at a type level, this can't occur /* let rvs = unsafe { valueset![ Value::new_bool(true), Value::new_iutf8("test1"), Value::new_iutf8("test2") ] }; let r3 = multi_value_boolean.validate_ava("mv_bool", &rvs); assert_eq!( r3, Err(SchemaError::InvalidAttributeSyntax("mv_bool".to_string())) ); */ let rvs = vs_bool![true, false]; let r4 = multi_value_boolean.validate_ava("mv_bool", &(rvs as _)); assert_eq!(r4, Ok(())); // syntax_id and index_type values let single_value_syntax = SchemaAttribute { // class: vec![String::from("attributetype")], name: AttrString::from("sv_syntax"), uuid: Uuid::new_v4(), description: String::from(""), multivalue: false, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::SYNTAX_ID, }; let rvs = vs_syntax![SyntaxType::try_from("UTF8STRING").unwrap()] as _; let r6 = single_value_syntax.validate_ava("sv_syntax", &rvs); assert_eq!(r6, Ok(())); let rvs = vs_utf8!["thaeountaheu".to_string()] as _; let r7 = single_value_syntax.validate_ava("sv_syntax", &rvs); assert_eq!( r7, Err(SchemaError::InvalidAttributeSyntax("sv_syntax".to_string())) ); let single_value_index = SchemaAttribute { // class: vec![String::from("attributetype")], name: AttrString::from("sv_index"), uuid: Uuid::new_v4(), description: String::from(""), multivalue: false, unique: false, phantom: false, index: vec![IndexType::Equality], syntax: SyntaxType::INDEX_ID, }; // let rvs = vs_index![IndexType::try_from("EQUALITY").unwrap()] as _; let r8 = single_value_index.validate_ava("sv_index", &rvs); assert_eq!(r8, Ok(())); let rvs = vs_utf8!["thaeountaheu".to_string()] as _; let r9 = single_value_index.validate_ava("sv_index", &rvs); assert_eq!( r9, Err(SchemaError::InvalidAttributeSyntax("sv_index".to_string())) ); } #[test] fn test_schema_simple() { let schema = Schema::new().expect("failed to create schema"); let schema_ro = schema.read(); validate_schema!(schema_ro); } #[test] fn test_schema_entries() { let _ = crate::tracing_tree::test_init(); // Given an entry, assert it's schema is valid // We do let schema_outer = Schema::new().expect("failed to create schema"); let schema = schema_outer.read(); let e_no_uuid: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": {} }"#, ) .into_invalid_new() }; assert_eq!( e_no_uuid.validate(&schema), Err(SchemaError::MissingMustAttribute(vec!["uuid".to_string()])) ); let e_no_class: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"] } }"#, ) .into_invalid_new() }; assert_eq!(e_no_class.validate(&schema), Err(SchemaError::NoClassFound)); let e_bad_class: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "class": ["zzzzzz"] } }"#, ) .into_invalid_new() }; assert_eq!( e_bad_class.validate(&schema), Err(SchemaError::InvalidClass(vec!["zzzzzz".to_string()])) ); let e_attr_invalid: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "class": ["object", "attributetype"] } }"#, ) .into_invalid_new() }; let res = e_attr_invalid.validate(&schema); assert!(match res { Err(SchemaError::MissingMustAttribute(_)) => true, _ => false, }); let e_attr_invalid_may: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["testattr"], "description": ["testattr"], "multivalue": ["false"], "unique": ["false"], "syntax": ["UTF8STRING"], "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "zzzzz": ["zzzz"] } }"#, ) .into_invalid_new() }; assert_eq!( e_attr_invalid_may.validate(&schema), Err(SchemaError::InvalidAttribute("zzzzz".to_string())) ); let e_attr_invalid_syn: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["testattr"], "description": ["testattr"], "multivalue": ["zzzzz"], "unique": ["false"], "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "syntax": ["UTF8STRING"] } }"#, ) .into_invalid_new() }; assert_eq!( e_attr_invalid_syn.validate(&schema), Err(SchemaError::InvalidAttributeSyntax( "multivalue".to_string() )) ); // You may not have the phantom. let e_phantom: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["testattr"], "description": ["testattr"], "multivalue": ["true"], "unique": ["false"], "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "syntax": ["UTF8STRING"], "password_import": ["password"] } }"#, ) .into_invalid_new() }; assert!(e_phantom.validate(&schema).is_err()); let e_ok: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "class": ["object", "attributetype"], "attributename": ["testattr"], "description": ["testattr"], "multivalue": ["true"], "unique": ["false"], "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "syntax": ["UTF8STRING"] } }"#, ) .into_invalid_new() }; assert!(e_ok.validate(&schema).is_ok()); } #[test] fn test_schema_entry_validate() { // Check that entries can be normalised and validated sanely let schema_outer = Schema::new().expect("failed to create schema"); let schema = schema_outer.write_blocking(); // Check syntax to upper // check index to upper // insense to lower // attr name to lower let e_test: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "class": ["extensibleobject"], "name": ["TestPerson"], "syntax": ["utf8string"], "UUID": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "InDeX": ["equality"] } }"#, ) .into_invalid_new() }; let e_expect: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "class": ["extensibleobject"], "name": ["testperson"], "syntax": ["UTF8STRING"], "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "index": ["EQUALITY"] } }"#, ) .into_valid_new() }; let e_valid = e_test.validate(&schema).expect("validation failure"); assert_eq!(e_expect, e_valid); } #[test] fn test_schema_extensible() { let schema_outer = Schema::new().expect("failed to create schema"); let schema = schema_outer.read(); // Just because you are extensible, doesn't mean you can be lazy let e_extensible_bad: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "class": ["extensibleobject"], "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "multivalue": ["zzzz"] } }"#, ) .into_invalid_new() }; assert_eq!( e_extensible_bad.validate(&schema), Err(SchemaError::InvalidAttributeSyntax( "multivalue".to_string() )) ); // Extensible doesn't mean you can have the phantoms let e_extensible_phantom: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "class": ["extensibleobject"], "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "password_import": ["zzzz"] } }"#, ) .into_invalid_new() }; assert_eq!( e_extensible_phantom.validate(&schema), Err(SchemaError::PhantomAttribute("password_import".to_string())) ); let e_extensible: Entry = unsafe { Entry::unsafe_from_entry_str( r#"{ "attrs": { "class": ["extensibleobject"], "uuid": ["db237e8a-0079-4b8c-8a56-593b22aa44d1"], "multivalue": ["true"] } }"#, ) .into_invalid_new() }; /* Is okay because extensible! */ assert!(e_extensible.validate(&schema).is_ok()); } #[test] fn test_schema_filter_validation() { let schema_outer = Schema::new().expect("failed to create schema"); let schema = schema_outer.read(); // Test non existant attr name let f_mixed = filter_all!(f_eq("nonClAsS", PartialValue::new_class("attributetype"))); assert_eq!( f_mixed.validate(&schema), Err(SchemaError::InvalidAttribute("nonclass".to_string())) ); // test syntax of bool let f_bool = filter_all!(f_eq("multivalue", PartialValue::new_iutf8("zzzz"))); assert_eq!( f_bool.validate(&schema), Err(SchemaError::InvalidAttributeSyntax( "multivalue".to_string() )) ); // test insensitive values let f_insense = filter_all!(f_eq("class", PartialValue::new_class("AttributeType"))); assert_eq!( f_insense.validate(&schema), Ok(unsafe { filter_valid!(f_eq("class", PartialValue::new_class("attributetype"))) }) ); // Test the recursive structures validate let f_or_empty = filter_all!(f_or!([])); assert_eq!(f_or_empty.validate(&schema), Err(SchemaError::EmptyFilter)); let f_or = filter_all!(f_or!([f_eq("multivalue", PartialValue::new_iutf8("zzzz"))])); assert_eq!( f_or.validate(&schema), Err(SchemaError::InvalidAttributeSyntax( "multivalue".to_string() )) ); let f_or_mult = filter_all!(f_and!([ f_eq("class", PartialValue::new_class("attributetype")), f_eq("multivalue", PartialValue::new_iutf8("zzzzzzz")), ])); assert_eq!( f_or_mult.validate(&schema), Err(SchemaError::InvalidAttributeSyntax( "multivalue".to_string() )) ); // Test mixed case attr name - this is a pass, due to normalisation let f_or_ok = filter_all!(f_andnot(f_and!([ f_eq("Class", PartialValue::new_class("AttributeType")), f_sub("class", PartialValue::new_class("classtype")), f_pres("class") ]))); assert_eq!( f_or_ok.validate(&schema), Ok(unsafe { filter_valid!(f_andnot(f_and!([ f_eq("class", PartialValue::new_class("attributetype")), f_sub("class", PartialValue::new_class("classtype")), f_pres("class") ]))) }) ); } #[test] fn test_schema_class_phantom_reject() { // Check that entries can be normalised and validated sanely let schema_outer = Schema::new().expect("failed to create schema"); let mut schema = schema_outer.write_blocking(); assert!(schema.validate().len() == 0); // Attempt to create a class with a phantom attribute, should be refused. let class = SchemaClass { name: AttrString::from("testobject"), uuid: Uuid::new_v4(), description: String::from("test object"), systemmay: vec![AttrString::from("claim")], may: vec![], systemmust: vec![], must: vec![], }; assert!(schema.update_classes(vec![class]).is_ok()); assert!(schema.validate().len() == 1); } }