use std::sync::Arc; use crate::plugins::Plugin; use crate::prelude::*; pub struct ValueDeny {} impl Plugin for ValueDeny { fn id() -> &'static str { "plugin_value_deny" } #[instrument(level = "debug", name = "denied_names_pre_create_transform", skip_all)] #[allow(clippy::cognitive_complexity)] fn pre_create_transform( qs: &mut QueryServerWriteTransaction, cand: &mut Vec<Entry<EntryInvalid, EntryNew>>, _ce: &CreateEvent, ) -> Result<(), OperationError> { let denied_names = qs.denied_names(); if denied_names.is_empty() { // Nothing to check. return Ok(()); } let mut pass = true; for entry in cand { // If the entry doesn't have a uuid, it's invalid anyway and will fail schema. if let Some(e_uuid) = entry.get_uuid() { // SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to // assert that the break glass and system accounts are NOT subject to // this process. if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID { // These entries are exempt continue; } } if let Some(name) = entry.get_ava_single_iname(Attribute::Name) { if denied_names.contains(name) { pass = false; error!(?name, "name denied by system configuration"); } } } if pass { Ok(()) } else { Err(OperationError::ValueDenyName) } } fn pre_modify( qs: &mut QueryServerWriteTransaction, pre_cand: &[Arc<EntrySealedCommitted>], cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, _me: &ModifyEvent, ) -> Result<(), OperationError> { Self::modify(qs, pre_cand, cand) } fn pre_batch_modify( qs: &mut QueryServerWriteTransaction, pre_cand: &[Arc<EntrySealedCommitted>], cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, _me: &BatchModifyEvent, ) -> Result<(), OperationError> { Self::modify(qs, pre_cand, cand) } fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> { let denied_names = qs.denied_names().clone(); let mut results = Vec::with_capacity(0); for denied_name in denied_names { let filt = filter!(f_eq(Attribute::Name, PartialValue::new_iname(&denied_name))); match qs.internal_search(filt) { Ok(entries) => { for entry in entries { let e_uuid = entry.get_uuid(); // SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to // assert that the break glass accounts are NOT subject to this process. if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID { // These entries are exempt continue; } results.push(Err(ConsistencyError::DeniedName(e_uuid))); } } Err(err) => { error!(?err); results.push(Err(ConsistencyError::QueryServerSearchFailure)) } } } results } } impl ValueDeny { #[instrument(level = "debug", name = "denied_names_modify", skip_all)] fn modify( qs: &mut QueryServerWriteTransaction, pre_cand: &[Arc<EntrySealedCommitted>], cand: &mut [EntryInvalidCommitted], ) -> Result<(), OperationError> { let denied_names = qs.denied_names(); if denied_names.is_empty() { // Nothing to check. return Ok(()); } let mut pass = true; for (pre_entry, post_entry) in pre_cand.iter().zip(cand.iter()) { // If the entry doesn't have a uuid, it's invalid anyway and will fail schema. let e_uuid = pre_entry.get_uuid(); // SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to // assert that the break glass accounts are NOT subject to this process. if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID { // These entries are exempt continue; } let pre_name = pre_entry.get_ava_single_iname(Attribute::Name); let post_name = post_entry.get_ava_single_iname(Attribute::Name); if let Some(name) = post_name { // Only if the name is changing, and is denied. if pre_name != post_name && denied_names.contains(name) { pass = false; error!(?name, "name denied by system configuration"); } } } if pass { Ok(()) } else { Err(OperationError::ValueDenyName) } } } #[cfg(test)] mod tests { use crate::prelude::*; async fn setup_name_deny(server: &QueryServer) { let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap(); let me_inv_m = ModifyEvent::new_internal_invalid( filter!(f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone())), ModifyList::new_list(vec![ Modify::Present(Attribute::DeniedName, Value::new_iname("tobias")), Modify::Present(Attribute::DeniedName, Value::new_iname("ellie")), ]), ); assert!(server_txn.modify(&me_inv_m).is_ok()); assert!(server_txn.commit().is_ok()); } #[qs_test] async fn test_valuedeny_create(server: &QueryServer) { setup_name_deny(server).await; let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap(); let t_uuid = Uuid::new_v4(); assert!(server_txn .internal_create(vec![entry_init!( (Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::Person.to_value()), (Attribute::Name, Value::new_iname("tobias")), (Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Description, Value::new_utf8s("Tobias")), (Attribute::DisplayName, Value::new_utf8s("Tobias")) ),]) .is_err()); } #[qs_test] async fn test_valuedeny_modify(server: &QueryServer) { // Create an entry that has a name which will become denied to test how it // interacts. let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap(); let e_uuid = Uuid::new_v4(); assert!(server_txn .internal_create(vec![entry_init!( (Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::Person.to_value()), (Attribute::Name, Value::new_iname("ellie")), (Attribute::Uuid, Value::Uuid(e_uuid)), (Attribute::Description, Value::new_utf8s("Ellie Meow")), (Attribute::DisplayName, Value::new_utf8s("Ellie Meow")) ),]) .is_ok()); assert!(server_txn.commit().is_ok()); setup_name_deny(server).await; let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap(); // Attempt to mod ellie. // Can mod a different attribute assert!(server_txn .internal_modify_uuid( e_uuid, &ModifyList::new_purge_and_set(Attribute::DisplayName, Value::new_utf8s("tobias")) ) .is_ok()); // Can't mod to another invalid name. assert!(server_txn .internal_modify_uuid( e_uuid, &ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias")) ) .is_err()); // Can mod to a valid name. assert!(server_txn .internal_modify_uuid( e_uuid, &ModifyList::new_purge_and_set( Attribute::Name, Value::new_iname("miss_meowington") ) ) .is_ok()); // Now mod from the valid name to an invalid one. assert!(server_txn .internal_modify_uuid( e_uuid, &ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias")) ) .is_err()); assert!(server_txn.commit().is_ok()); } #[qs_test] async fn test_valuedeny_jpwarren_special(server: &QueryServer) { // Assert that our break glass accounts are exempt from this processing. let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap(); let me_inv_m = ModifyEvent::new_internal_invalid( filter!(f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone())), ModifyList::new_list(vec![ Modify::Present(Attribute::DeniedName, Value::new_iname("admin")), Modify::Present(Attribute::DeniedName, Value::new_iname("idm_admin")), ]), ); assert!(server_txn.modify(&me_inv_m).is_ok()); assert!(server_txn.commit().is_ok()); let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap(); assert!(server_txn .internal_modify_uuid( UUID_IDM_ADMIN, &ModifyList::new_purge_and_set( Attribute::DisplayName, Value::new_utf8s("Idm Admin") ) ) .is_ok()); assert!(server_txn .internal_modify_uuid( UUID_ADMIN, &ModifyList::new_purge_and_set(Attribute::DisplayName, Value::new_utf8s("Admin")) ) .is_ok()); assert!(server_txn.commit().is_ok()); } #[qs_test] async fn test_valuedeny_batch_modify(server: &QueryServer) { setup_name_deny(server).await; let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap(); let t_uuid = Uuid::new_v4(); assert!(server_txn .internal_create(vec![entry_init!( (Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::Person.to_value()), (Attribute::Name, Value::new_iname("newname")), (Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Description, Value::new_utf8s("Tobias")), (Attribute::DisplayName, Value::new_utf8s("Tobias")) ),]) .is_ok()); // Now batch mod assert!(server_txn .internal_batch_modify( [( t_uuid, ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias")) )] .into_iter() ) .is_err()); } }