diff --git a/book/src/integrations/oauth2.md b/book/src/integrations/oauth2.md index 54d09bcf8..6aaac2f26 100644 --- a/book/src/integrations/oauth2.md +++ b/book/src/integrations/oauth2.md @@ -215,7 +215,7 @@ Token signing public key ### Create the Kanidm Configuration -By default, members of the `system_admins` or `idm_hp_oauth2_manage_priv` groups are able to create +By default, members of the `idm_admins` or `idm_oauth2_admins` groups are able to create or manage OAuth2 client integrations. You can create a new client by specifying its client name, application display name and the landing diff --git a/book/src/supported_features.md b/book/src/supported_features.md index 20895a41a..d2ef9a53f 100644 --- a/book/src/supported_features.md +++ b/book/src/supported_features.md @@ -22,6 +22,7 @@ This is a list of supported features and standards within Kanidm. - [RFC4519 LDAP Schema](https://www.rfc-editor.org/rfc/rfc4519) - FreeIPA User Schema - [RFC7644 SCIM Bulk Data Import](https://www.rfc-editor.org/rfc/rfc7644) + - NOTE: SCIM is only supported for synchronisation from another IDP at this time. # Database diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs index 8055ca055..1471d9757 100644 --- a/server/lib/src/constants/schema.rs +++ b/server/lib/src/constants/schema.rs @@ -219,6 +219,16 @@ pub static ref SCHEMA_ATTR_DENIED_NAME: SchemaAttribute = SchemaAttribute { ..Default::default() }; +pub static ref SCHEMA_ATTR_DENIED_NAME_DL10: SchemaAttribute = SchemaAttribute { + uuid: UUID_SCHEMA_ATTR_DENIED_NAME, + name: Attribute::DeniedName, + description: "Iname values that are not allowed to be used in 'name'.".to_string(), + + syntax: SyntaxType::Utf8StringIname, + multivalue: true, + ..Default::default() +}; + pub static ref SCHEMA_ATTR_DOMAIN_TOKEN_KEY: SchemaAttribute = SchemaAttribute { uuid: UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY, name: Attribute::DomainTokenKey, diff --git a/server/lib/src/plugins/valuedeny.rs b/server/lib/src/plugins/valuedeny.rs index e0a360ee0..6c96bce12 100644 --- a/server/lib/src/plugins/valuedeny.rs +++ b/server/lib/src/plugins/valuedeny.rs @@ -10,7 +10,7 @@ impl Plugin for ValueDeny { "plugin_value_deny" } - #[instrument(level = "debug", name = "base_pre_create_transform", skip_all)] + #[instrument(level = "debug", name = "denied_names_pre_create_transform", skip_all)] #[allow(clippy::cognitive_complexity)] fn pre_create_transform( qs: &mut QueryServerWriteTransaction, @@ -19,9 +19,25 @@ impl Plugin for ValueDeny { ) -> 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; @@ -37,27 +53,24 @@ impl Plugin for ValueDeny { } } - #[instrument(level = "debug", name = "base_pre_modify", skip_all)] fn pre_modify( qs: &mut QueryServerWriteTransaction, - _pre_cand: &[Arc<EntrySealedCommitted>], + pre_cand: &[Arc<EntrySealedCommitted>], cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, _me: &ModifyEvent, ) -> Result<(), OperationError> { - Self::modify(qs, cand) + Self::modify(qs, pre_cand, cand) } - #[instrument(level = "debug", name = "base_pre_modify", skip_all)] fn pre_batch_modify( qs: &mut QueryServerWriteTransaction, - _pre_cand: &[Arc<EntrySealedCommitted>], + pre_cand: &[Arc<EntrySealedCommitted>], cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, _me: &BatchModifyEvent, ) -> Result<(), OperationError> { - Self::modify(qs, cand) + Self::modify(qs, pre_cand, cand) } - #[instrument(level = "debug", name = "base::verify", skip_all)] fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> { let denied_names = qs.denied_names().clone(); @@ -68,7 +81,15 @@ impl Plugin for ValueDeny { match qs.internal_search(filt) { Ok(entries) => { for entry in entries { - results.push(Err(ConsistencyError::DeniedName(entry.get_uuid()))); + 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) => { @@ -83,17 +104,37 @@ impl Plugin for ValueDeny { } impl ValueDeny { + #[instrument(level = "debug", name = "denied_names_modify", skip_all)] fn modify( qs: &mut QueryServerWriteTransaction, - cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, + 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 entry in cand { - if let Some(name) = entry.get_ava_single_iname(Attribute::Name) { - if denied_names.contains(name) { + 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"); } @@ -117,10 +158,10 @@ mod tests { 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"), - )]), + 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()); @@ -148,30 +189,103 @@ mod tests { #[qs_test] async fn test_valuedeny_modify(server: &QueryServer) { - setup_name_deny(server).await; - + // 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 t_uuid = Uuid::new_v4(); + 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("newname")), - (Attribute::Uuid, Value::Uuid(t_uuid)), - (Attribute::Description, Value::new_utf8s("Tobias")), - (Attribute::DisplayName, Value::new_utf8s("Tobias")) + (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()); - // Now mod it + 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( - t_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] diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index 43af61b5f..b7673fb6f 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -427,7 +427,10 @@ impl QueryServerWriteTransaction<'_> { // =========== Apply changes ============== // Now update schema - let idm_schema_changes = [SCHEMA_CLASS_DOMAIN_INFO_DL10.clone().into()]; + let idm_schema_changes = [ + SCHEMA_ATTR_DENIED_NAME_DL10.clone().into(), + SCHEMA_CLASS_DOMAIN_INFO_DL10.clone().into(), + ]; idm_schema_changes .into_iter()