kanidm/server/lib/src/plugins/valuedeny.rs
Firstyear ed88b72080
Exempt idm_admin and admin from denied names. ()
idm_admin and admin should be exempted from the denied names process,
as these values will already be denied due to attribute uniqueness.
Additionally improved the denied names check to only validate the
name during a change, not during a modifification. This way entries
that become denied can get themself out of the pickle.
2025-02-15 22:45:25 +00:00

322 lines
11 KiB
Rust

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());
}
}