kanidm/server/lib/src/plugins/base.rs
William Brown 0fe42f62bd Progress
2025-04-11 09:30:44 +10:00

778 lines
27 KiB
Rust

use std::collections::BTreeSet;
use std::iter::once;
use std::sync::Arc;
use hashbrown::HashSet;
use crate::event::{CreateEvent, ModifyEvent};
use crate::modify::Modify;
use crate::plugins::Plugin;
use crate::prelude::*;
// This module has some special properties around it's operation, namely that it
// has to make a certain number of assertions *early* in the entry lifecycle around
// names and uuids since these have such significance to every other part of the
// servers operation. As a result, this is the ONLY PLUGIN that does validation in the
// pre_create_transform step, where every other SHOULD use the post_* hooks for all
// validation operations.
//
// Additionally, this plugin WILL block and deny certain modifications to uuids and
// more to prevent intentional DB damage.
pub struct Base {}
impl Plugin for Base {
fn id() -> &'static str {
"plugin_base"
}
#[instrument(level = "debug", name = "base_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> {
// debug!("Entering base pre_create_transform");
// For each candidate
for entry in cand.iter_mut() {
// First, ensure we have the 'object', class in the class set.
entry.add_ava(Attribute::Class, EntryClass::Object.to_value());
// if they don't have uuid, create it.
match entry.get_ava_set(Attribute::Uuid).map(|s| s.len()) {
None => {
// Generate
let ava_uuid = Value::Uuid(Uuid::new_v4());
trace!("Setting temporary UUID {:?} to entry", ava_uuid);
entry.set_ava(&Attribute::Uuid, once(ava_uuid));
}
Some(1) => {
// Do nothing
}
Some(x) => {
// If we get some it MUST be 2 +
admin_error!(
"Entry defines {} attr, but has multiple ({}) values.",
Attribute::Uuid,
x
);
return Err(OperationError::Plugin(PluginError::Base(
"Uuid has multiple values".to_string(),
)));
}
};
}
// Now, every cand has a UUID - create a cand uuid set from it.
let mut cand_uuid: BTreeSet<Uuid> = BTreeSet::new();
let mut system_range_invalid = false;
// As we insert into the set, if a duplicate is found, return an error
// that a duplicate exists.
//
// Remember, we have to use the ava here, not the get_uuid types because
// we may not have filled in the uuid field yet.
for entry in cand.iter_mut() {
let uuid_ref: Uuid = entry
.get_ava_single_uuid(Attribute::Uuid)
.ok_or_else(|| OperationError::InvalidAttribute(Attribute::Uuid.to_string()))?;
// Check that the system-protected range is not in the cand_uuid, unless we are
// an internal operation.
if uuid_ref < DYNAMIC_RANGE_MINIMUM_UUID {
if ce.ident.is_internal() {
// it's a builtin entry, lets add the class.
entry.add_ava(Attribute::Class, EntryClass::Builtin.to_value());
} else {
// Don't do that!
error!(
"uuid from protected system UUID range found in create set! {:?}",
uuid_ref
);
system_range_invalid = true;
}
};
if !cand_uuid.insert(uuid_ref) {
trace!("uuid duplicate found in create set! {:?}", uuid_ref);
return Err(OperationError::Plugin(PluginError::Base(
"Uuid duplicate detected in request".to_string(),
)));
}
}
if system_range_invalid {
return Err(OperationError::Plugin(PluginError::Base(
"Uuid must not be in protected range".to_string(),
)));
}
if cand_uuid.contains(&UUID_DOES_NOT_EXIST) {
error!(
"uuid \"does not exist\" found in create set! THIS IS A BUG. PLEASE REPORT IT IMMEDIATELY."
);
return Err(OperationError::Plugin(PluginError::Base(
"Attempt to create UUID_DOES_NOT_EXIST".to_string(),
)));
}
// Now from each element, generate a filter to search for all of them
//
// IMPORTANT: We don't exclude recycled or tombstones here!
let filt_in = filter_all!(FC::Or(
cand_uuid
.into_iter()
.map(|u| FC::Eq(Attribute::Uuid, PartialValue::Uuid(u)))
.collect(),
));
// If any results exist, fail as a duplicate UUID is present.
// TODO #69: Can we report which UUID exists? Probably yes, we do
// internal search and report the UUID *OR* we alter internal_exists
// to return UUID sets. This can be done as an extension to #69 where the
// internal exists is actually a wrapper around a search for uuid internally
//
// But does it add value? How many people will try to custom define/add uuid?
let r = qs.internal_exists(filt_in);
match r {
Ok(b) => {
if b {
admin_error!("A UUID already exists, rejecting.");
return Err(OperationError::Plugin(PluginError::Base(
"Uuid duplicate found in database".to_string(),
)));
}
}
Err(e) => {
admin_error!("Error occurred checking UUID existence. {:?}", e);
return Err(e);
}
}
Ok(())
}
#[instrument(level = "debug", name = "base_pre_modify", skip_all)]
fn pre_modify(
_qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
_cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
me: &ModifyEvent,
) -> Result<(), OperationError> {
me.modlist.iter().try_for_each(|modify| {
let attr = match &modify {
Modify::Present(a, _)
| Modify::Removed(a, _)
| Modify::Purged(a)
| Modify::Set(a, _) => Some(a),
Modify::Assert(_, _) => None,
};
if attr == Some(&Attribute::Uuid) {
debug!(?modify, "Modify in violation");
request_error!("Modifications to UUID's are NOT ALLOWED");
Err(OperationError::SystemProtectedAttribute)
} else {
Ok(())
}
})
}
#[instrument(level = "debug", name = "base_pre_modify", skip_all)]
fn pre_batch_modify(
_qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
_cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
me: &BatchModifyEvent,
) -> Result<(), OperationError> {
me.modset
.values()
.flat_map(|ml| ml.iter())
.try_for_each(|modify| {
let attr = match &modify {
Modify::Present(a, _)
| Modify::Removed(a, _)
| Modify::Set(a, _)
| Modify::Purged(a) => Some(a),
Modify::Assert(_, _) => None,
};
if attr == Some(&Attribute::Uuid) {
debug!(?modify, "Modify in violation");
request_error!("Modifications to UUID's are NOT ALLOWED");
Err(OperationError::SystemProtectedAttribute)
} else {
Ok(())
}
})
}
#[instrument(level = "debug", name = "base::verify", skip_all)]
fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
// Search for class = *
let entries = match qs.internal_search(filter!(f_pres(Attribute::Class))) {
Ok(v) => v,
Err(e) => {
admin_error!("Internal Search Failure: {:?}", e);
return vec![Err(ConsistencyError::QueryServerSearchFailure)];
}
};
let mut uuid_seen: HashSet<Uuid> = HashSet::with_capacity(entries.len());
entries
.iter()
// do an exists checks on the uuid
.map(|e| {
// To get the entry deserialised, a UUID MUST EXIST, else an expect
// will be thrown in the deserialise (possibly it will be better
// handled later). But it means this check only needs to validate
// uniqueness!
let uuid = e.get_uuid();
if uuid_seen.insert(uuid) {
// Insert returns true if the item was unique.
Ok(())
} else {
Err(ConsistencyError::UuidNotUnique(uuid.to_string()))
}
})
.filter(|v| v.is_err())
.collect()
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use std::sync::Arc;
const UUID_TEST_ACCOUNT: Uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
const UUID_TEST_GROUP: Uuid = uuid::uuid!("81ec1640-3637-4a2f-8a52-874fa3c3c92f");
const UUID_TEST_ACP: Uuid = uuid::uuid!("acae81d6-5ea7-4bd8-8f7f-fcec4c0dd647");
lazy_static! {
pub static ref TEST_ACCOUNT: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::MemberOf.to_value()),
(Attribute::Name, Value::new_iname("test_account_1")),
(Attribute::DisplayName, Value::new_utf8s("test_account_1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT)),
(Attribute::MemberOf, Value::Refer(UUID_TEST_GROUP))
);
pub static ref TEST_GROUP: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("test_group_a")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP)),
(Attribute::Member, Value::Refer(UUID_TEST_ACCOUNT))
);
pub static ref ALLOW_ALL: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(
Attribute::Class,
EntryClass::AccessControlProfile.to_value()
),
(
Attribute::Class,
EntryClass::AccessControlTargetScope.to_value()
),
(
Attribute::Class,
EntryClass::AccessControlReceiverGroup.to_value()
),
(Attribute::Class, EntryClass::AccessControlModify.to_value()),
(Attribute::Class, EntryClass::AccessControlCreate.to_value()),
(Attribute::Class, EntryClass::AccessControlDelete.to_value()),
(Attribute::Class, EntryClass::AccessControlSearch.to_value()),
(
Attribute::Name,
Value::new_iname("idm_admins_acp_allow_all_test")
),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACP)),
(Attribute::AcpReceiverGroup, Value::Refer(UUID_TEST_GROUP)),
(
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"pres\":\"class\"}").expect("filter")
),
(Attribute::AcpSearchAttr, Value::from(Attribute::Name)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Class)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Uuid)),
(Attribute::AcpModifyClass, EntryClass::System.to_value()),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyRemovedAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Must)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyPresentAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Must)
),
(Attribute::AcpCreateClass, EntryClass::Object.to_value()),
(Attribute::AcpCreateClass, EntryClass::Person.to_value()),
(Attribute::AcpCreateClass, EntryClass::System.to_value()),
(Attribute::AcpCreateAttr, Value::from(Attribute::Name)),
(Attribute::AcpCreateAttr, Value::from(Attribute::Class)),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::Description)
),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpCreateAttr, Value::from(Attribute::Uuid))
);
pub static ref PRELOAD: Vec<EntryInitNew> =
vec![TEST_ACCOUNT.clone(), TEST_GROUP.clone(), ALLOW_ALL.clone()];
pub static ref E_TEST_ACCOUNT: Arc<EntrySealedCommitted> =
Arc::new(TEST_ACCOUNT.clone().into_sealed_committed());
}
// check create where no uuid
#[test]
fn test_pre_create_no_uuid() {
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::with_capacity(0);
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("Test Person".to_string())
)
);
let create = vec![e];
run_create_test!(
Ok(None),
preload,
create,
None,
|qs: &mut QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson")
)))
.expect("Internal search failure");
let ue = cands.first().expect("No cand");
assert!(ue.attribute_pres(Attribute::Uuid));
}
);
}
// check unparsable uuid
#[test]
fn test_pre_create_uuid_invalid() {
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::with_capacity(0);
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("Test Person".to_string())
),
(Attribute::Uuid, Value::Utf8("xxxxxx".to_string()))
);
let create = vec![e];
run_create_test!(
Err(OperationError::InvalidAttribute(
Attribute::Uuid.to_string()
)),
preload,
create,
None,
|_| {}
);
}
// check entry where uuid is empty list
#[test]
fn test_pre_create_uuid_empty() {
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::with_capacity(0);
let mut e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("Test Person".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("79724141-3603-4060-b6bb-35c72772611d"))
)
);
let vs = e.get_ava_mut(Attribute::Uuid).unwrap();
vs.clear();
let create = vec![e.clone()];
run_create_test!(
Err(OperationError::Plugin(PluginError::Base(
"Uuid format invalid".to_string()
))),
preload,
create,
None,
|_| {}
);
}
// check create where provided uuid is valid. It should be unchanged.
#[test]
fn test_pre_create_uuid_valid() {
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::with_capacity(0);
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("Test Person".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("79724141-3603-4060-b6bb-35c72772611d"))
)
);
let create = vec![e];
run_create_test!(
Ok(None),
preload,
create,
None,
|qs: &mut QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson")
)))
.expect("Internal search failure");
let ue = cands.first().expect("No cand");
assert!(ue.attribute_equality(
Attribute::Uuid,
&PartialValue::Uuid(uuid!("79724141-3603-4060-b6bb-35c72772611d"))
));
}
);
}
#[test]
fn test_pre_create_uuid_valid_multi() {
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::with_capacity(0);
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("Test Person".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("79724141-3603-4060-b6bb-35c72772611e"))
),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("79724141-3603-4060-b6bb-35c72772611d"))
)
);
let create = vec![e];
run_create_test!(
Err(OperationError::Plugin(PluginError::Base(
"Uuid has multiple values".to_string()
))),
preload,
create,
None,
|_| {}
);
}
// check create where uuid already exists.
// -- check create where uuid is a well-known
// This second case is technically handled as well-known
// types are created "at startup" so it's not possible
// to create one.
//
// To solidify this, we could make a range of min-max well knowns
// to ensure we always have a name space to draw from?
#[test]
fn test_pre_create_uuid_exist() {
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("Test Person".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("79724141-3603-4060-b6bb-35c72772611d"))
)
);
let create = vec![e.clone()];
let preload = vec![e];
run_create_test!(
Err(OperationError::Plugin(PluginError::Base(
"Uuid duplicate found in database".to_string()
))),
preload,
create,
None,
|_| {}
);
}
#[test]
fn test_pre_create_double_uuid() {
// Test adding two entries with the same uuid
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::with_capacity(0);
let ea = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("Test Person".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("79724141-3603-4060-b6bb-35c72772611d"))
)
);
let eb = ea.clone();
let create = vec![ea, eb];
run_create_test!(
Err(OperationError::Plugin(PluginError::Base(
"Uuid duplicate detected in request".to_string()
))),
preload,
create,
None,
|_| {}
);
}
// All of these *SHOULD* be blocked?
#[test]
fn test_modify_uuid_present() {
// Add another uuid to a type
let ea = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("79724141-3603-4060-b6bb-35c72772611d"))
)
);
let preload = vec![ea];
run_modify_test!(
Err(OperationError::SystemProtectedAttribute),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_a")
)),
ModifyList::new_list(vec![Modify::Present(
Attribute::Uuid,
Value::from("f15a7219-1d15-44e3-a7b4-bec899c07788")
)]),
None,
|_| {},
|_| {}
);
}
#[test]
fn test_modify_uuid_removed() {
// Test attempting to remove a uuid
let ea = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("79724141-3603-4060-b6bb-35c72772611d"))
)
);
let preload = vec![ea];
run_modify_test!(
Err(OperationError::SystemProtectedAttribute),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_a")
)),
ModifyList::new_list(vec![Modify::Removed(
Attribute::Uuid,
PartialValue::Uuid(uuid!("f15a7219-1d15-44e3-a7b4-bec899c07788"))
)]),
None,
|_| {},
|_| {}
);
}
#[test]
fn test_modify_uuid_purged() {
// Test attempting to purge uuid
let ea = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("79724141-3603-4060-b6bb-35c72772611d"))
)
);
let preload = vec![ea];
run_modify_test!(
Err(OperationError::SystemProtectedAttribute),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_a")
)),
ModifyList::new_list(vec![Modify::Purged(Attribute::Uuid)]),
None,
|_| {},
|_| {}
);
}
#[test]
fn test_protected_uuid_range() {
// Test an external create, it should fail.
// Testing internal create is not super needed, due to migrations at start
// up testing this every time we run :P
let preload = PRELOAD.clone();
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::DisplayName, Value::new_iname("testperson")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("00000000-0000-0000-0000-f0f0f0f0f0f0"))
)
);
let create = vec![e];
run_create_test!(
Err(OperationError::Plugin(PluginError::Base(
"Uuid must not be in protected range".to_string()
))),
preload,
create,
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
#[test]
fn test_protected_uuid_range_2() {
// Test an external create, it should fail.
// Testing internal create is not super needed, due to migrations at start
// up testing this every time we run :P
let preload = PRELOAD.clone();
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::DisplayName, Value::new_iname("testperson")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("00000000-0000-0000-0000-f0f0f0f0f0f0"))
)
);
let create = vec![e];
run_create_test!(
Err(OperationError::Plugin(PluginError::Base(
"Uuid must not be in protected range".to_string()
))),
preload,
create,
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
#[test]
fn test_protected_uuid_does_not_exist() {
// Test that internal create of "does not exist" will fail.
let preload = Vec::with_capacity(0);
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::DisplayName, Value::new_iname("testperson")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("00000000-0000-0000-0000-fffffffffffe"))
)
);
let create = vec![e];
run_create_test!(
Err(OperationError::Plugin(PluginError::Base(
"UUID_DOES_NOT_EXIST may not exist!".to_string()
))),
preload,
create,
None,
|_| {}
);
}
}