// A plugin that generates gid numbers on types that require them for posix // support. use std::iter::once; use std::sync::Arc; use crate::event::{CreateEvent, ModifyEvent}; use crate::plugins::Plugin; use crate::prelude::*; use crate::utils::uuid_to_gid_u32; // Systemd dynamic units allocate between 61184–65519, most distros allocate // system uids from 0 - 1000, and many others give user ids between 1000 to // 2000. This whole numberspace is cursed, lets assume it's not ours. :( // // Per <https://systemd.io/UIDS-GIDS/>, systemd claims a huge chunk of this // space to itself. As a result we can't allocate between 65536 and u32 max // because systemd takes most of the usable range for its own containers, // and half the range is probably going to trigger linux kernel issues. // // Seriously, linux's uid/gid model is so fundamentally terrible... Windows // NT got this right with SIDs. // // Because of this, we have to ensure that anything we allocate is in the // range 1879048192 (0x70000000) to 2147483647 (0x7fffffff) const GID_SYSTEM_NUMBER_PREFIX: u32 = 0x7000_0000; const GID_SYSTEM_NUMBER_MASK: u32 = 0x0fff_ffff; // Systemd claims so many ranges to itself, we have to check we are in certain bounds. // // This is the normal system range, we MUST NOT allow it to be allocated. pub const GID_REGULAR_USER_MIN: u32 = 1000; pub const GID_REGULAR_USER_MAX: u32 = 60000; // Systemd homed claims 60001 through 60577 pub const GID_UNUSED_A_MIN: u32 = 60578; pub const GID_UNUSED_A_MAX: u32 = 61183; // Systemd dyn service users 61184 through 65519 pub const GID_UNUSED_B_MIN: u32 = 65520; pub const GID_UNUSED_B_MAX: u32 = 65533; // nobody is 65534 // 16bit uid -1 65535 pub const GID_UNUSED_C_MIN: u32 = 65536; const GID_UNUSED_C_MAX: u32 = 524287; // systemd claims 524288 through 1879048191 for nspawn const GID_NSPAWN_MIN: u32 = 524288; const GID_NSPAWN_MAX: u32 = 1879048191; const GID_UNUSED_D_MIN: u32 = 0x7000_0000; pub const GID_UNUSED_D_MAX: u32 = 0x7fff_ffff; // Anything above 2147483648 can confuse the kernel (so basically half the address space // can't be accessed. // const GID_UNSAFE_MAX: u32 = 2147483648; pub struct GidNumber {} fn apply_gidnumber<T: Clone>(e: &mut Entry<EntryInvalid, T>) -> Result<(), OperationError> { if (e.attribute_equality(Attribute::Class, &EntryClass::PosixGroup.into()) || e.attribute_equality(Attribute::Class, &EntryClass::PosixAccount.into())) && !e.attribute_pres(Attribute::GidNumber) { let u_ref = e .get_uuid() .ok_or(OperationError::InvalidEntryState) .inspect_err(|_e| { admin_error!("Invalid Entry State - Missing UUID"); })?; let gid = uuid_to_gid_u32(u_ref); // Apply the mask to only take the last 24 bits, and then move them // to the correct range. let gid = gid & GID_SYSTEM_NUMBER_MASK; let gid = gid | GID_SYSTEM_NUMBER_PREFIX; let gid_v = Value::new_uint32(gid); admin_info!("Generated {} for {:?}", gid, u_ref); e.set_ava(&Attribute::GidNumber, once(gid_v)); Ok(()) } else if let Some(gid) = e.get_ava_single_uint32(Attribute::GidNumber) { // If they provided us with a gid number, ensure it's in a safe range. if (GID_REGULAR_USER_MIN..=GID_REGULAR_USER_MAX).contains(&gid) || (GID_UNUSED_A_MIN..=GID_UNUSED_A_MAX).contains(&gid) || (GID_UNUSED_B_MIN..= GID_UNUSED_B_MAX).contains(&gid) || (GID_UNUSED_C_MIN..=GID_UNUSED_C_MAX).contains(&gid) // We won't ever generate an id in the nspawn range, but we do secretly allow // it to be set for compatibility with services like freeipa or openldap. TBH // most people don't even use systemd nspawn anyway ... // // I made this design choice to avoid a tunable that may confuse people to // its purpose. This way things "just work" for imports and existing systems // but we do the right thing in the future. || (GID_NSPAWN_MIN..=GID_NSPAWN_MAX).contains(&gid) || (GID_UNUSED_D_MIN..=GID_UNUSED_D_MAX).contains(&gid) { Ok(()) } else { // Note that here we don't advertise that we allow the nspawn range to be set, even // though we do allow it. error!( "Requested GID ({}) overlaps a system range. Allowed ranges are {} to {}, {} to {} and {} to {}", gid, GID_REGULAR_USER_MIN, GID_REGULAR_USER_MAX, GID_UNUSED_C_MIN, GID_UNUSED_C_MAX, GID_UNUSED_D_MIN, GID_UNUSED_D_MAX ); Err(OperationError::PL0001GidOverlapsSystemRange) } } else { Ok(()) } } impl Plugin for GidNumber { fn id() -> &'static str { "plugin_gidnumber" } #[instrument(level = "debug", name = "gidnumber_pre_create_transform", skip_all)] fn pre_create_transform( _qs: &mut QueryServerWriteTransaction, cand: &mut Vec<Entry<EntryInvalid, EntryNew>>, _ce: &CreateEvent, ) -> Result<(), OperationError> { cand.iter_mut().try_for_each(apply_gidnumber) } #[instrument(level = "debug", name = "gidnumber_pre_modify", skip_all)] fn pre_modify( _qs: &mut QueryServerWriteTransaction, _pre_cand: &[Arc<EntrySealedCommitted>], cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, _me: &ModifyEvent, ) -> Result<(), OperationError> { cand.iter_mut().try_for_each(apply_gidnumber) } #[instrument(level = "debug", name = "gidnumber_pre_batch_modify", skip_all)] fn pre_batch_modify( _qs: &mut QueryServerWriteTransaction, _pre_cand: &[Arc<EntrySealedCommitted>], cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, _me: &BatchModifyEvent, ) -> Result<(), OperationError> { cand.iter_mut().try_for_each(apply_gidnumber) } } #[cfg(test)] mod tests { use super::{ GID_REGULAR_USER_MAX, GID_REGULAR_USER_MIN, GID_UNUSED_A_MAX, GID_UNUSED_A_MIN, GID_UNUSED_B_MAX, GID_UNUSED_B_MIN, GID_UNUSED_C_MIN, GID_UNUSED_D_MAX, }; use crate::prelude::*; #[qs_test] async fn test_gidnumber_generate(server: &QueryServer) { let mut server_txn = server.write(duration_from_epoch_now()).await.expect("txn"); // Test that the gid number is generated on create { let user_a_uuid = uuid!("83a0927f-3de1-45ec-bea0-2f7b997ef244"); let op_result = server_txn.internal_create(vec![entry_init!( (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::PosixAccount.to_value()), (Attribute::Name, Value::new_iname("testperson_1")), (Attribute::Uuid, Value::Uuid(user_a_uuid)), (Attribute::Description, Value::new_utf8s("testperson")), (Attribute::DisplayName, Value::new_utf8s("testperson")) )]); assert!(op_result.is_ok()); let user_a = server_txn .internal_search_uuid(user_a_uuid) .expect("Unable to access user"); let user_a_uid = user_a .get_ava_single_uint32(Attribute::GidNumber) .expect("gidnumber not present on account"); assert_eq!(user_a_uid, 0x797ef244); } // test that gid is not altered if provided on create. let user_b_uuid = uuid!("d90fb0cb-6785-4f36-94cb-e364d9c13255"); { let op_result = server_txn.internal_create(vec![entry_init!( (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::PosixAccount.to_value()), (Attribute::Name, Value::new_iname("testperson_2")), (Attribute::Uuid, Value::Uuid(user_b_uuid)), (Attribute::GidNumber, Value::Uint32(10001)), (Attribute::Description, Value::new_utf8s("testperson")), (Attribute::DisplayName, Value::new_utf8s("testperson")) )]); assert!(op_result.is_ok()); let user_b = server_txn .internal_search_uuid(user_b_uuid) .expect("Unable to access user"); let user_b_uid = user_b .get_ava_single_uint32(Attribute::GidNumber) .expect("gidnumber not present on account"); assert_eq!(user_b_uid, 10001); } // Test that if the value is deleted, it is correctly regenerated. { let modlist = modlist!([m_purge(Attribute::GidNumber)]); server_txn .internal_modify_uuid(user_b_uuid, &modlist) .expect("Unable to modify user"); let user_b = server_txn .internal_search_uuid(user_b_uuid) .expect("Unable to access user"); let user_b_uid = user_b .get_ava_single_uint32(Attribute::GidNumber) .expect("gidnumber not present on account"); assert_eq!(user_b_uid, 0x79c13255); } let user_c_uuid = uuid!("0d5086b0-74f9-4518-92b4-89df0c55971b"); // Test that an entry when modified to have posix attributes will have // it's gidnumber generated. { let op_result = server_txn.internal_create(vec![entry_init!( (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::Person.to_value()), (Attribute::Name, Value::new_iname("testperson_3")), (Attribute::Uuid, Value::Uuid(user_c_uuid)), (Attribute::Description, Value::new_utf8s("testperson")), (Attribute::DisplayName, Value::new_utf8s("testperson")) )]); assert!(op_result.is_ok()); let user_c = server_txn .internal_search_uuid(user_c_uuid) .expect("Unable to access user"); assert_eq!(user_c.get_ava_single_uint32(Attribute::GidNumber), None); let modlist = modlist!([m_pres( Attribute::Class, &EntryClass::PosixAccount.to_value() )]); server_txn .internal_modify_uuid(user_c_uuid, &modlist) .expect("Unable to modify user"); let user_c = server_txn .internal_search_uuid(user_c_uuid) .expect("Unable to access user"); let user_c_uid = user_c .get_ava_single_uint32(Attribute::GidNumber) .expect("gidnumber not present on account"); assert_eq!(user_c_uid, 0x7c55971b); } let user_d_uuid = uuid!("36dc9010-d80c-404b-b5ba-8f66657c2f1d"); // Test that an entry when modified to have posix attributes will have // it's gidnumber generated. { let op_result = server_txn.internal_create(vec![entry_init!( (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::Person.to_value()), (Attribute::Name, Value::new_iname("testperson_4")), (Attribute::Uuid, Value::Uuid(user_d_uuid)), (Attribute::Description, Value::new_utf8s("testperson")), (Attribute::DisplayName, Value::new_utf8s("testperson")) )]); assert!(op_result.is_ok()); let user_d = server_txn .internal_search_uuid(user_d_uuid) .expect("Unable to access user"); assert_eq!(user_d.get_ava_single_uint32(Attribute::GidNumber), None); let modlist = modlist!([m_pres( Attribute::Class, &EntryClass::PosixAccount.to_value() )]); server_txn .internal_modify_uuid(user_d_uuid, &modlist) .expect("Unable to modify user"); let user_d = server_txn .internal_search_uuid(user_d_uuid) .expect("Unable to access user"); let user_d_uid = user_d .get_ava_single_uint32(Attribute::GidNumber) .expect("gidnumber not present on account"); assert_eq!(user_d_uid, 0x757c2f1d); } let user_e_uuid = uuid!("a6dc0d68-9c7a-4dad-b1e2-f6274b691373"); // Test that an entry when modified to have posix attributes, if a gidnumber // is provided then it is respected. { let op_result = server_txn.internal_create(vec![entry_init!( (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::Person.to_value()), (Attribute::Name, Value::new_iname("testperson_5")), (Attribute::Uuid, Value::Uuid(user_e_uuid)), (Attribute::Description, Value::new_utf8s("testperson")), (Attribute::DisplayName, Value::new_utf8s("testperson")) )]); assert!(op_result.is_ok()); let user_e = server_txn .internal_search_uuid(user_e_uuid) .expect("Unable to access user"); assert_eq!(user_e.get_ava_single_uint32(Attribute::GidNumber), None); let modlist = modlist!([ m_pres(Attribute::Class, &EntryClass::PosixAccount.to_value()), m_pres(Attribute::GidNumber, &Value::Uint32(10002)) ]); server_txn .internal_modify_uuid(user_e_uuid, &modlist) .expect("Unable to modify user"); let user_e = server_txn .internal_search_uuid(user_e_uuid) .expect("Unable to access user"); let user_e_uid = user_e .get_ava_single_uint32(Attribute::GidNumber) .expect("gidnumber not present on account"); assert_eq!(user_e_uid, 10002); } // Test rejection of important gid values. let user_f_uuid = uuid!("33afc396-2434-47e5-b143-05176148b50e"); // Test that an entry when modified to have posix attributes, if a gidnumber // is provided then it is respected. { let op_result = server_txn.internal_create(vec![entry_init!( (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::Person.to_value()), (Attribute::Name, Value::new_iname("testperson_6")), (Attribute::Uuid, Value::Uuid(user_f_uuid)), (Attribute::Description, Value::new_utf8s("testperson")), (Attribute::DisplayName, Value::new_utf8s("testperson")) )]); assert!(op_result.is_ok()); for id in [ 0, 500, GID_REGULAR_USER_MIN - 1, GID_REGULAR_USER_MAX + 1, GID_UNUSED_A_MIN - 1, GID_UNUSED_A_MAX + 1, GID_UNUSED_B_MIN - 1, GID_UNUSED_B_MAX + 1, GID_UNUSED_C_MIN - 1, GID_UNUSED_D_MAX + 1, u32::MAX, ] { let modlist = modlist!([ m_pres(Attribute::Class, &EntryClass::PosixAccount.to_value()), m_pres(Attribute::GidNumber, &Value::Uint32(id)) ]); let op_result = server_txn.internal_modify_uuid(user_f_uuid, &modlist); trace!(?id); assert_eq!(op_result, Err(OperationError::PL0001GidOverlapsSystemRange)); } } assert!(server_txn.commit().is_ok()); } }