//! Modification expressions and validation. This is how `ModifyEvents` store and //! express the series of Modifications that should be applied. These are expressed //! as "states" on what attribute-values should appear as within the `Entry` use std::slice; use kanidm_proto::internal::{ Modify as ProtoModify, ModifyList as ProtoModifyList, OperationError, SchemaError, }; use kanidm_proto::v1::Entry as ProtoEntry; // Should this be std? use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use crate::prelude::*; use crate::schema::SchemaTransaction; use crate::value::{PartialValue, Value}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ModifyValid; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ModifyInvalid; #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum Modify { /// This value *should* exist for this attribute. Present(Attribute, Value), /// This value *should not* exist for this attribute. Removed(Attribute, PartialValue), /// This attr should not exist, and if it does exist, will have all content removed. Purged(Attribute), /// This attr and value must exist *in this state* for this change to proceed. Assert(Attribute, PartialValue), /// Set and replace the entire content of an attribute. This requires both presence /// and removal access to the attribute to proceed. Set(Attribute, ValueSet), } pub fn m_pres(attr: Attribute, v: &Value) -> Modify { Modify::Present(attr, v.clone()) } pub fn m_remove(attr: Attribute, v: &PartialValue) -> Modify { Modify::Removed(attr, v.clone()) } pub fn m_purge(attr: Attribute) -> Modify { Modify::Purged(attr) } pub fn m_assert(attr: Attribute, v: &PartialValue) -> Modify { Modify::Assert(attr, v.clone()) } impl Modify { pub fn from( m: &ProtoModify, qs: &mut QueryServerWriteTransaction, ) -> Result { Ok(match m { ProtoModify::Present(a, v) => { let a = Attribute::from(a.as_str()); let v = qs.clone_value(&a, v)?; Modify::Present(a, v) } ProtoModify::Removed(a, v) => { let a = Attribute::from(a.as_str()); let v = qs.clone_partialvalue(&a, v)?; Modify::Removed(a, v) } ProtoModify::Purged(a) => Modify::Purged(Attribute::from(a.as_str())), }) } } #[derive(Clone, Debug, Default)] pub struct ModifyList { // This is never read, it's just used for state machine enforcement. #[allow(dead_code)] valid: VALID, // The order of this list matters. Each change must be done in order. mods: Vec, } impl<'a> IntoIterator for &'a ModifyList { type IntoIter = slice::Iter<'a, Modify>; type Item = &'a Modify; fn into_iter(self) -> Self::IntoIter { self.mods.iter() } } impl ModifyList { pub fn new() -> Self { ModifyList { valid: ModifyInvalid, mods: Vec::with_capacity(0), } } pub fn new_list(mods: Vec) -> Self { ModifyList { valid: ModifyInvalid, mods, } } pub fn new_purge_and_set(attr: Attribute, v: Value) -> Self { Self::new_list(vec![m_purge(attr.clone()), Modify::Present(attr, v)]) } pub fn new_append(attr: Attribute, v: Value) -> Self { Self::new_list(vec![Modify::Present(attr, v)]) } pub fn new_remove(attr: Attribute, pv: PartialValue) -> Self { Self::new_list(vec![Modify::Removed(attr, pv)]) } pub fn new_purge(attr: Attribute) -> Self { Self::new_list(vec![m_purge(attr)]) } pub fn new_set(attr: Attribute, vs: ValueSet) -> Self { Self::new_list(vec![Modify::Set(attr, vs)]) } pub fn push_mod(&mut self, modify: Modify) { self.mods.push(modify) } pub fn from( ml: &ProtoModifyList, qs: &mut QueryServerWriteTransaction, ) -> Result { // For each ProtoModify, do a from. let inner: Result, _> = ml.mods.iter().map(|pm| Modify::from(pm, qs)).collect(); match inner { Ok(m) => Ok(ModifyList { valid: ModifyInvalid, mods: m, }), Err(e) => Err(e), } } pub fn from_patch( pe: &ProtoEntry, qs: &mut QueryServerWriteTransaction, ) -> Result { let mut mods = Vec::with_capacity(0); pe.attrs.iter().try_for_each(|(attr, vals)| { // Issue a purge to the attr. let attr: Attribute = attr.as_str().into(); mods.push(m_purge(attr.clone())); // Now if there are vals, push those too. // For each value we want to now be present. vals.iter().try_for_each(|val| { qs.clone_value(&attr, val).map(|resolved_v| { mods.push(Modify::Present(attr.clone(), resolved_v)); }) }) })?; Ok(ModifyList { valid: ModifyInvalid, mods, }) } pub fn validate( &self, schema: &dyn SchemaTransaction, ) -> Result, SchemaError> { let schema_attributes = schema.get_attributes(); /* let schema_name = schema_attributes .get(Attribute::Name.as_ref()") .expect("Critical: Core schema corrupt or missing. To initiate a core transfer, please deposit substitute core in receptacle."); */ let res: Result, _> = self .mods .iter() .map(|m| match m { Modify::Present(attr, value) => match schema_attributes.get(attr) { Some(schema_a) => schema_a .validate_value(attr, value) .map(|_| Modify::Present(attr.clone(), value.clone())), None => Err(SchemaError::InvalidAttribute(attr.to_string())), }, Modify::Removed(attr, value) => match schema_attributes.get(attr) { Some(schema_a) => schema_a .validate_partialvalue(attr, value) .map(|_| Modify::Removed(attr.clone(), value.clone())), None => Err(SchemaError::InvalidAttribute(attr.to_string())), }, Modify::Assert(attr, value) => match schema_attributes.get(attr) { Some(schema_a) => schema_a .validate_partialvalue(attr, value) .map(|_| Modify::Assert(attr.clone(), value.clone())), None => Err(SchemaError::InvalidAttribute(attr.to_string())), }, Modify::Purged(attr) => match schema_attributes.get(attr) { Some(_attr_name) => Ok(Modify::Purged(attr.clone())), None => Err(SchemaError::InvalidAttribute(attr.to_string())), }, Modify::Set(attr, valueset) => match schema_attributes.get(attr) { Some(_attr_name) => Ok(Modify::Set(attr.clone(), valueset.clone())), None => Err(SchemaError::InvalidAttribute(attr.to_string())), }, }) .collect(); let valid_mods = res?; // Return new ModifyList! Ok(ModifyList { valid: ModifyValid, mods: valid_mods, }) } /// ⚠️ - Convert a modlist to be considered valid, bypassing schema. /// This is a TEST ONLY method and will never be exposed in production. #[cfg(test)] pub(crate) fn into_valid(self) -> ModifyList { ModifyList { valid: ModifyValid, mods: self.mods, } } } impl From>> for ModifyList { fn from(attrs: BTreeMap>) -> Self { let mods = attrs .into_iter() .map(|(attr, maybe_valueset)| { if let Some(valueset) = maybe_valueset { Modify::Set(attr, valueset) } else { Modify::Purged(attr) } }) .collect(); ModifyList { valid: ModifyInvalid, mods, } } } impl ModifyList { /// ⚠️ - Create a new modlist that is considered valid, bypassing schema. /// This is a TEST ONLY method and will never be exposed in production. #[cfg(test)] pub fn new_valid_list(mods: Vec) -> Self { ModifyList { valid: ModifyValid, mods, } } pub fn iter(&self) -> slice::Iter { self.mods.iter() } } impl ModifyList { pub fn len(&self) -> usize { self.mods.len() } pub fn is_empty(&self) -> bool { self.len() == 0 } }