diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index be677d614..cb6e11dcb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -25,7 +25,7 @@ - Yuxuan Lu (leoleoasd) - h7x4 - Pi-Cla -- Sebastiano Tocci(Seba-T) +- Sebastiano Tocci (Seba-T) - Minh Phan (MinhPhan8803) - Kenton Groombridge (0xC0ncord) - Martin Weinelt (hexa) diff --git a/book/src/developers/designs/replication.rst b/book/src/developers/designs/replication.rst index 115b65d81..ef12b0982 100644 --- a/book/src/developers/designs/replication.rst +++ b/book/src/developers/designs/replication.rst @@ -92,7 +92,7 @@ state must be resolved in a manner consistent to schema of the system. An additional complexity is that both servers must be able to resolve this conflict in isolation, without further communication. All servers must arrive -at the same result, necesitating a set of conflict management rules that must +at the same result, necessitating a set of conflict management rules that must be the same to all members of the replication topology. As a result, almost all facets of replication are designed around the possible diff --git a/server/lib/src/be/dbvalue.rs b/server/lib/src/be/dbvalue.rs index 6a8b7b48d..36e5fc8b1 100644 --- a/server/lib/src/be/dbvalue.rs +++ b/server/lib/src/be/dbvalue.rs @@ -11,6 +11,7 @@ use webauthn_rs::prelude::{ use webauthn_rs_core::proto::{COSEKey, UserVerificationPolicy}; // Re-export this as though it was here. +use crate::repl::cid::Cid; pub use kanidm_lib_crypto::DbPasswordV1; #[derive(Serialize, Deserialize, Debug)] @@ -608,6 +609,8 @@ pub enum DbValueSetV2 { TotpSecret(Vec<(String, DbTotpV1)>), #[serde(rename = "AT")] ApiToken(Vec), + #[serde(rename = "SA")] + AuditLogString(Vec<(Cid, String)>), } impl DbValueSetV2 { @@ -650,6 +653,7 @@ impl DbValueSetV2 { DbValueSetV2::JwsKeyRs256(set) => set.len(), DbValueSetV2::UiHint(set) => set.len(), DbValueSetV2::TotpSecret(set) => set.len(), + DbValueSetV2::AuditLogString(set) => set.len(), } } diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs index f2622031d..94a75bb36 100644 --- a/server/lib/src/constants/schema.rs +++ b/server/lib/src/constants/schema.rs @@ -171,6 +171,41 @@ pub const JSON_SCHEMA_ATTR_LEGALNAME: &str = r#"{ ] } }"#; + +pub const JSON_SCHEMA_ATTR_NAME_HISTORY: &str = r#"{ + "attrs": { + "class": [ + "object", + "system", + "attributetype" + ], + "description": [ + "The history of names that a person has had" + ], + "index": [ + "EQUALITY" + ], + "unique": [ + "false" + ], + "multivalue": [ + "true" + ], + "sync_allowed": [ + "true" + ], + "attributename": [ + "name_history" + ], + "syntax": [ + "AUDIT_LOG_STRING" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000133" + ] + } +}"#; + pub const JSON_SCHEMA_ATTR_RADIUS_SECRET: &str = r#"{ "attrs": { "class": [ @@ -1578,7 +1613,8 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#" "oauth2_consent_scope_map", "user_auth_token_session", "oauth2_session", - "description" + "description", + "name_history" ], "systemmust": [ "displayname", diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs index 8efbae02f..360959f1b 100644 --- a/server/lib/src/constants/uuids.rs +++ b/server/lib/src/constants/uuids.rs @@ -227,6 +227,7 @@ pub const UUID_SCHEMA_ATTR_PRIVATE_COOKIE_KEY: Uuid = uuid!("00000000-0000-0000- pub const _UUID_SCHEMA_ATTR_DOMAIN_LDAP_BASEDN: Uuid = uuid!("00000000-0000-0000-0000-ffff00000131"); pub const UUID_SCHEMA_ATTR_DYNMEMBER: Uuid = uuid!("00000000-0000-0000-0000-ffff00000132"); +pub const UUID_SCHEMA_ATTR_NAME_HISTORY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000133"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. diff --git a/server/lib/src/plugins/base.rs b/server/lib/src/plugins/base.rs index 3debbf234..673ddcdfb 100644 --- a/server/lib/src/plugins/base.rs +++ b/server/lib/src/plugins/base.rs @@ -12,7 +12,7 @@ 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 signifigance to every other part of the +// 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. diff --git a/server/lib/src/plugins/mod.rs b/server/lib/src/plugins/mod.rs index 1650ada09..d1fd8d439 100644 --- a/server/lib/src/plugins/mod.rs +++ b/server/lib/src/plugins/mod.rs @@ -19,6 +19,7 @@ pub(crate) mod dyngroup; mod gidnumber; mod jwskeygen; mod memberof; +mod namehistory; mod protected; mod refint; mod session; @@ -207,6 +208,7 @@ impl Plugins { .and_then(|_| gidnumber::GidNumber::pre_create_transform(qs, cand, ce)) .and_then(|_| domain::Domain::pre_create_transform(qs, cand, ce)) .and_then(|_| spn::Spn::pre_create_transform(qs, cand, ce)) + .and_then(|_| namehistory::NameHistory::pre_create_transform(qs, cand, ce)) // Should always be last .and_then(|_| attrunique::AttrUnique::pre_create_transform(qs, cand, ce)) } @@ -245,6 +247,7 @@ impl Plugins { .and_then(|_| domain::Domain::pre_modify(qs, pre_cand, cand, me)) .and_then(|_| spn::Spn::pre_modify(qs, pre_cand, cand, me)) .and_then(|_| session::SessionConsistency::pre_modify(qs, pre_cand, cand, me)) + .and_then(|_| namehistory::NameHistory::pre_modify(qs, pre_cand, cand, me)) // attr unique should always be last .and_then(|_| attrunique::AttrUnique::pre_modify(qs, pre_cand, cand, me)) } @@ -276,6 +279,7 @@ impl Plugins { .and_then(|_| domain::Domain::pre_batch_modify(qs, pre_cand, cand, me)) .and_then(|_| spn::Spn::pre_batch_modify(qs, pre_cand, cand, me)) .and_then(|_| session::SessionConsistency::pre_batch_modify(qs, pre_cand, cand, me)) + .and_then(|_| namehistory::NameHistory::pre_batch_modify(qs, pre_cand, cand, me)) // attr unique should always be last .and_then(|_| attrunique::AttrUnique::pre_batch_modify(qs, pre_cand, cand, me)) } diff --git a/server/lib/src/plugins/namehistory.rs b/server/lib/src/plugins/namehistory.rs new file mode 100644 index 000000000..7d2030a20 --- /dev/null +++ b/server/lib/src/plugins/namehistory.rs @@ -0,0 +1,267 @@ +use std::sync::Arc; + +use kanidm_proto::v1::OperationError; + +use crate::entry::{EntryInvalidCommitted, EntrySealedCommitted}; +use crate::event::ModifyEvent; +use crate::plugins::Plugin; +use crate::prelude::*; +use crate::prelude::{BatchModifyEvent, QueryServerWriteTransaction}; +use crate::repl::cid::Cid; +use crate::value::PartialValue; + +pub struct NameHistory {} + +lazy_static! { + // it contains all the partialvalues used to match against an Entry's class, + // we just need a partialvalue to match in order to target the entry + static ref CLASSES_TO_UPDATE: [PartialValue; 1] = [PartialValue::new_iutf8("account")]; + static ref HISTORY_ATTRIBUTES: [&'static str;1] = ["name"]; +} + +impl NameHistory { + fn is_entry_to_update(entry: &mut Entry) -> bool { + CLASSES_TO_UPDATE + .iter() + .any(|pv| entry.attribute_equality("class", pv)) + } + + fn get_ava_name(history_attr: &str) -> String { + format!("{}_history", history_attr) + } + + fn handle_name_updates( + pre_cand: &[Arc], + cand: &mut Vec, + cid: &Cid, + ) -> Result<(), OperationError> { + for (pre, post) in pre_cand.iter().zip(cand) { + // here we check if the current entry has at least one of the classes we intend to target + if Self::is_entry_to_update(post) { + for history_attr in HISTORY_ATTRIBUTES.iter() { + let pre_name_option = pre.get_ava_single(history_attr); + let post_name_option = post.get_ava_single(history_attr); + if let (Some(pre_name), Some(post_name)) = (pre_name_option, post_name_option) { + if pre_name != post_name { + let ava_name = Self::get_ava_name(history_attr); + //// WARNING!!! this match will have to be adjusted based on what kind of attribute + //// we are matching on, for example for displayname we would have to use Value::utf8 instead!! + // as of now we're interested just in the name so we use Iname + match post_name { + Value::Iname(n) => post.add_ava_if_not_exist( + &ava_name, + Value::AuditLogString(cid.clone(), n), + ), + _ => return Err(OperationError::InvalidValueState), + } + } + } + } + } + } + Ok(()) + } + + fn handle_name_creation( + cands: &mut Vec, + cid: &Cid, + ) -> Result<(), OperationError> { + for cand in cands.iter_mut() { + if Self::is_entry_to_update(cand) { + for history_attr in HISTORY_ATTRIBUTES.iter() { + if let Some(name) = cand.get_ava_single(history_attr) { + let ava_name = Self::get_ava_name(history_attr); + match name { + Value::Iname(n) => cand.add_ava_if_not_exist( + &ava_name, + Value::AuditLogString(cid.clone(), n), + ), + _ => return Err(OperationError::InvalidValueState), + } + } + } + } + } + + Ok(()) + } +} + +impl Plugin for NameHistory { + fn id() -> &'static str { + "plugin_name_history" + } + + fn pre_create_transform( + qs: &mut QueryServerWriteTransaction, + cand: &mut Vec, + _ce: &CreateEvent, + ) -> Result<(), OperationError> { + Self::handle_name_creation(cand, qs.get_txn_cid()) + } + + fn pre_modify( + qs: &mut QueryServerWriteTransaction, + pre_cand: &[Arc], + cand: &mut Vec, + _me: &ModifyEvent, + ) -> Result<(), OperationError> { + Self::handle_name_updates(pre_cand, cand, qs.get_txn_cid()) + } + + fn pre_batch_modify( + qs: &mut QueryServerWriteTransaction, + pre_cand: &[Arc], + cand: &mut Vec, + _me: &BatchModifyEvent, + ) -> Result<(), OperationError> { + Self::handle_name_updates(pre_cand, cand, qs.get_txn_cid()) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use crate::entry::{Entry, EntryInit, EntryNew}; + use crate::prelude::uuid; + use crate::repl::cid::Cid; + use crate::value::Value; + + #[test] + fn name_purge_and_set() { + // Add another uuid to a type + let cid = Cid::new( + uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"), + Duration::new(20, 2), + ); + let ea = entry_init!( + ("class", Value::new_class("account")), + ("class", Value::new_class("posixaccount")), + ("name", Value::new_iname("old_name")), + ( + "uuid", + Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee")) + ), + ( + "name_history", + Value::new_audit_log_string((cid.clone(), "old_name".to_string())).unwrap() + ), + ("description", Value::new_utf8s("testperson")), + ("displayname", Value::new_utf8s("old name person")) + ); + let preload = vec![ea]; + run_modify_test!( + Ok(()), + preload, + filter!(f_eq("name", PartialValue::new_iname("old_name"))), + modlist!([ + m_purge("name"), + m_pres("name", &Value::new_iname("new_name_1")) + ]), + None, + |_| {}, + |qs: &mut QueryServerWriteTransaction| { + let e = qs + .internal_search_uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee")) + .expect("failed to get entry"); + let c = e + .get_ava_set("name_history") + .expect("failed to get primary cred."); + dbg!(c.clone()); + return assert!( + c.contains(&PartialValue::new_utf8s("old_name")) + && c.contains(&PartialValue::new_utf8s("new_name_1")) + ); + } + ); + } + + #[test] + fn name_creation() { + // Add another uuid to a type + let ea = entry_init!( + ("class", Value::new_class("account")), + ("class", Value::new_class("posixaccount")), + ("name", Value::new_iname("old_name")), + ( + "uuid", + Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47e1")) + ), + ("description", Value::new_utf8s("testperson")), + ("displayname", Value::new_utf8s("old name person")) + ); + let preload = Vec::new(); + let create = vec![ea]; + run_create_test!( + Ok(()), + preload, + create, + None, + |qs: &mut QueryServerWriteTransaction| { + let e = qs + .internal_search_uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47e1")) + .expect("failed to get entry"); + dbg!(e.get_ava()); + let name_history = e + .get_ava_set("name_history") + .expect("failed to get name_history ava"); + + return assert!(name_history.contains(&PartialValue::new_utf8s(&"old_name"))); + } + ); + } + + #[test] + fn name_purge_and_set_with_filled_history() { + let mut cids: Vec = Vec::new(); + for i in 1..8 { + cids.push(Cid::new( + uuid!("d2b496bd-8493-47b7-8142-f568b5cf47e1"), + Duration::new(20 + i, 0), + )) + } + // Add another uuid to a type + let mut ea = entry_init!( + ("class", Value::new_class("account")), + ("class", Value::new_class("posixaccount")), + ("name", Value::new_iname("old_name8")), + ( + "uuid", + Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee")) + ), + ("description", Value::new_utf8s("testperson")), + ("displayname", Value::new_utf8s("old name person")) + ); + for (i, cid) in cids.iter().enumerate() { + let index = 1 + i; + let name = format!("old_name{index}"); + ea.add_ava("name_history", Value::AuditLogString(cid.clone(), name)) + } + let preload = vec![ea]; + run_modify_test!( + Ok(()), + preload, + filter!(f_eq("name", PartialValue::new_iname("old_name8"))), + modlist!([ + m_purge("name"), + m_pres("name", &Value::new_iname("new_name")) + ]), + None, + |_| {}, + |qs: &mut QueryServerWriteTransaction| { + let e = qs + .internal_search_uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee")) + .expect("failed to get entry"); + dbg!(e.get_ava()); + let c = e + .get_ava_set("name_history") + .expect("failed to get name_history ava :/"); + return assert!( + !c.contains(&PartialValue::new_utf8s(&"old_name1")) + && c.contains(&PartialValue::new_utf8s(&"new_name")) + ); + } + ); + } +} diff --git a/server/lib/src/repl/proto.rs b/server/lib/src/repl/proto.rs index 953af3c8a..d355c733d 100644 --- a/server/lib/src/repl/proto.rs +++ b/server/lib/src/repl/proto.rs @@ -375,6 +375,9 @@ pub enum ReplAttrV1 { TotpSecret { set: Vec<(String, ReplTotpV1)>, }, + AuditLogString { + set: Vec<(Cid, String)>, + }, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] diff --git a/server/lib/src/schema.rs b/server/lib/src/schema.rs index 72f341040..e49320e2d 100644 --- a/server/lib/src/schema.rs +++ b/server/lib/src/schema.rs @@ -213,6 +213,7 @@ impl SchemaAttribute { SyntaxType::UiHint => matches!(v, PartialValue::UiHint(_)), // Comparing on the label. SyntaxType::TotpSecret => matches!(v, PartialValue::Utf8(_)), + SyntaxType::AuditLogString => matches!(v, PartialValue::Utf8(_)), }; if r { Ok(()) @@ -262,6 +263,7 @@ impl SchemaAttribute { SyntaxType::JwsKeyRs256 => matches!(v, Value::JwsKeyRs256(_)), SyntaxType::UiHint => matches!(v, Value::UiHint(_)), SyntaxType::TotpSecret => matches!(v, Value::TotpSecret(_, _)), + SyntaxType::AuditLogString => matches!(v, Value::Utf8(_)), }; if r { Ok(()) diff --git a/server/lib/src/server/create.rs b/server/lib/src/server/create.rs index 1fb008375..b25779d29 100644 --- a/server/lib/src/server/create.rs +++ b/server/lib/src/server/create.rs @@ -200,6 +200,11 @@ mod tests { e.add_ava("directmemberof", Value::Refer(UUID_IDM_ALL_PERSONS)); e.add_ava("memberof", Value::Refer(UUID_IDM_ALL_ACCOUNTS)); e.add_ava("directmemberof", Value::Refer(UUID_IDM_ALL_ACCOUNTS)); + // we also add the name_history ava! + e.add_ava( + "name_history", + Value::AuditLogString(server_txn.get_txn_cid().clone(), "testperson".to_string()), + ); let expected = unsafe { vec![Arc::new(e.into_sealed_committed())] }; diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index dafa315d9..e874e1280 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -423,6 +423,7 @@ impl<'a> QueryServerWriteTransaction<'a> { let idm_schema: Vec<&str> = vec![ JSON_SCHEMA_ATTR_DISPLAYNAME, JSON_SCHEMA_ATTR_LEGALNAME, + JSON_SCHEMA_ATTR_NAME_HISTORY, JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_SSH_PUBLICKEY, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL, diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index 0f380fa61..5a42bdd7d 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -4,28 +4,20 @@ use std::str::FromStr; use std::sync::Arc; -use crate::prelude::*; - use concread::arcache::{ARCache, ARCacheBuilder, ARCacheReadTxn}; use concread::cowcell::*; use hashbrown::HashSet; -use kanidm_proto::v1::{ConsistencyError, UiHint}; use tokio::sync::{Semaphore, SemaphorePermit}; use tracing::trace; -use self::access::{ - profiles::{ - AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch, - }, - AccessControls, AccessControlsReadTransaction, AccessControlsTransaction, - AccessControlsWriteTransaction, -}; +use kanidm_proto::v1::{ConsistencyError, UiHint}; use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction}; // We use so many, we just import them all ... use crate::filter::{Filter, FilterInvalid, FilterValid, FilterValidResolved}; use crate::plugins::dyngroup::{DynGroup, DynGroupCache}; use crate::plugins::Plugins; +use crate::prelude::*; use crate::repl::cid::Cid; use crate::repl::proto::ReplRuvRange; use crate::repl::ruv::ReplicationUpdateVectorTransaction; @@ -36,6 +28,14 @@ use crate::schema::{ use crate::value::EXTRACT_VAL_DN; use crate::valueset::uuid_to_proto_string; +use self::access::{ + profiles::{ + AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch, + }, + AccessControls, AccessControlsReadTransaction, AccessControlsTransaction, + AccessControlsWriteTransaction, +}; + pub mod access; pub mod batch_modify; pub mod create; @@ -543,6 +543,7 @@ pub trait QueryServerTransaction<'a> { .map(Value::UiHint) .map_err(|()| OperationError::InvalidAttribute("Invalid uihint syntax".to_string())), SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute("TotpSecret Values can not be supplied through modification".to_string())), + SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute("Audit logs are generated and not able to be set.".to_string())), } } None => { @@ -649,6 +650,7 @@ pub trait QueryServerTransaction<'a> { .map_err(|()| { OperationError::InvalidAttribute("Invalid uihint syntax".to_string()) }), + SyntaxType::AuditLogString => Ok(PartialValue::new_utf8s(value)), } } None => { @@ -1540,11 +1542,13 @@ impl<'a> QueryServerWriteTransaction<'a> { .and_then(|_| accesscontrols.commit()) .and_then(|_| be_txn.commit()) } + pub(crate) fn get_txn_cid(&self) -> &Cid { + &self.cid + } } #[cfg(test)] mod tests { - use crate::prelude::*; #[qs_test] diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs index f4bdfb74a..754b5c365 100644 --- a/server/lib/src/value.rs +++ b/server/lib/src/value.rs @@ -3,8 +3,6 @@ //! typed values, allows their comparison, filtering and more. It also has the code for serialising //! these into a form for the backend that can be persistent into the [`Backend`](crate::be::Backend). -use crate::prelude::*; - use std::collections::BTreeSet; use std::convert::TryFrom; use std::fmt; @@ -12,15 +10,10 @@ use std::fmt::Formatter; use std::str::FromStr; use std::time::Duration; -use crate::valueset::uuid_to_proto_string; #[cfg(test)] use base64::{engine::general_purpose, Engine as _}; use compact_jwt::JwsSigner; use hashbrown::HashSet; -use kanidm_proto::v1::ApiTokenPurpose; -use kanidm_proto::v1::Filter as ProtoFilter; -use kanidm_proto::v1::UatPurposeStatus; -use kanidm_proto::v1::UiHint; use num_enum::TryFromPrimitive; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -30,10 +23,17 @@ use url::Url; use uuid::Uuid; use webauthn_rs::prelude::{DeviceKey as DeviceKeyV4, Passkey as PasskeyV4}; +use kanidm_proto::v1::ApiTokenPurpose; +use kanidm_proto::v1::Filter as ProtoFilter; +use kanidm_proto::v1::UatPurposeStatus; +use kanidm_proto::v1::UiHint; + use crate::be::dbentry::DbIdentSpn; use crate::credential::{totp::Totp, Credential}; +use crate::prelude::*; use crate::repl::cid::Cid; use crate::server::identity::IdentityId; +use crate::valueset::uuid_to_proto_string; lazy_static! { pub static ref SPN_RE: Regex = { @@ -239,6 +239,7 @@ pub enum SyntaxType { UiHint = 29, TotpSecret = 30, ApiToken = 31, + AuditLogString = 32, } impl TryFrom<&str> for SyntaxType { @@ -280,6 +281,7 @@ impl TryFrom<&str> for SyntaxType { "UIHINT" => Ok(SyntaxType::UiHint), "TOTPSECRET" => Ok(SyntaxType::TotpSecret), "APITOKEN" => Ok(SyntaxType::ApiToken), + "AUDIT_LOG_STRING" => Ok(SyntaxType::AuditLogString), _ => Err(()), } } @@ -320,6 +322,7 @@ impl fmt::Display for SyntaxType { SyntaxType::UiHint => "UIHINT", SyntaxType::TotpSecret => "TOTPSECRET", SyntaxType::ApiToken => "APITOKEN", + SyntaxType::AuditLogString => "AUDIT_LOG_STRING", }) } } @@ -887,6 +890,7 @@ pub enum Value { UiHint(UiHint), TotpSecret(String, Totp), + AuditLogString(Cid, String), } impl PartialEq for Value { @@ -1089,6 +1093,10 @@ impl Value { bool::from_str(s).map(Value::Bool).ok() } + pub fn new_audit_log_string(e: (Cid, String)) -> Option { + Some(Value::AuditLogString(e.0, e.1)) + } + #[inline] pub fn is_bool(&self) -> bool { matches!(self, Value::Bool(_)) @@ -1653,7 +1661,9 @@ impl Value { Value::ApiToken(_, at) => { Value::validate_str_escapes(&at.label) && Value::validate_singleline(&at.label) } - + Value::AuditLogString(_, s) => { + Value::validate_str_escapes(s) && Value::validate_singleline(s) + } // These have stricter validators so not needed. Value::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s), Value::DateTime(odt) => odt.offset() == time::UtcOffset::UTC, diff --git a/server/lib/src/valueset/auditlogstring.rs b/server/lib/src/valueset/auditlogstring.rs new file mode 100644 index 000000000..f4bfe31e0 --- /dev/null +++ b/server/lib/src/valueset/auditlogstring.rs @@ -0,0 +1,152 @@ +use smolset::SmolSet; + +use crate::prelude::*; +use crate::repl::cid::Cid; +use crate::repl::proto::ReplAttrV1; +use crate::schema::SchemaAttribute; +use crate::valueset::{DbValueSetV2, ValueSet}; + +type AuditLogStringType = (Cid, String); + +#[derive(Debug, Clone)] +pub struct ValueSetAuditLogString { + set: SmolSet<[AuditLogStringType; 8]>, +} + +impl ValueSetAuditLogString { + fn remove_oldest(&mut self) { + let oldest = self.set.iter().min().cloned(); + if let Some(oldest_value) = oldest { + self.set.remove(&oldest_value); + } + } + + pub fn new(s: AuditLogStringType) -> Box { + let mut set = SmolSet::new(); + set.insert(s); + Box::new(ValueSetAuditLogString { set }) + } + + pub fn push(&mut self, s: AuditLogStringType) -> bool { + self.set.insert(s) + } + + pub fn from_dbvs2(data: Vec) -> Result { + let set = data.into_iter().collect(); + Ok(Box::new(ValueSetAuditLogString { set })) + } + + pub fn from_repl_v1(data: &[AuditLogStringType]) -> Result { + let set = data.iter().map(|e| (e.0.clone(), e.1.clone())).collect(); + Ok(Box::new(ValueSetAuditLogString { set })) + } +} + +impl ValueSetT for ValueSetAuditLogString { + fn insert_checked(&mut self, value: Value) -> Result { + match value { + Value::AuditLogString(c, s) => { + if self.set.len() >= 8 { + self.remove_oldest(); + } + Ok(self.push((c, s))) + } + _ => { + debug_assert!(false); + Err(OperationError::InvalidValueState) + } + } + } + + fn clear(&mut self) { + self.set.clear(); + } + + fn remove(&mut self, _pv: &PartialValue) -> bool { + false + } + + fn contains(&self, pv: &PartialValue) -> bool { + match pv { + PartialValue::Utf8(s) => self.set.iter().any(|(_, current)| s.eq(current)), + _ => { + debug_assert!(false); + true + } + } + } + + fn substring(&self, _pv: &PartialValue) -> bool { + false + } + + fn lessthan(&self, _pv: &PartialValue) -> bool { + false + } + + fn len(&self) -> usize { + self.set.len() + } + + fn generate_idx_eq_keys(&self) -> Vec { + self.set.iter().map(|(d, s)| format!("{d}-{s}")).collect() + } + + fn syntax(&self) -> SyntaxType { + SyntaxType::AuditLogString + } + + fn validate(&self, _schema_attr: &SchemaAttribute) -> bool { + self.set + .iter() + .all(|(_, s)| Value::validate_str_escapes(s) && Value::validate_singleline(s)) + && self.set.len() <= 8 + } + + fn to_proto_string_clone_iter(&self) -> Box + '_> { + Box::new(self.set.iter().map(|(d, s)| format!("{d}-{s}"))) + } + + fn to_db_valueset_v2(&self) -> DbValueSetV2 { + DbValueSetV2::AuditLogString(self.set.iter().cloned().collect()) + } + + fn to_repl_v1(&self) -> ReplAttrV1 { + ReplAttrV1::AuditLogString { + set: self.set.iter().cloned().collect(), + } + } + + fn to_partialvalue_iter(&self) -> Box + '_> { + Box::new(self.set.iter().map(|(_, s)| PartialValue::Utf8(s.clone()))) + } + + fn to_value_iter(&self) -> Box + '_> { + Box::new( + self.set + .iter() + .map(|(c, s)| Value::AuditLogString(c.clone(), s.clone())), + ) + } + + fn equal(&self, other: &ValueSet) -> bool { + if let Some(other) = other.as_audit_log_string() { + &self.set == other + } else { + debug_assert!(false); + false + } + } + + fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> { + if let Some(b) = other.as_audit_log_string() { + mergesets!(self.set, b) + } else { + debug_assert!(false); + Err(OperationError::InvalidValueState) + } + } + fn as_audit_log_string(&self) -> Option<&SmolSet<[(Cid, String); 8]>> { + Some(&self.set) + } +} diff --git a/server/lib/src/valueset/mod.rs b/server/lib/src/valueset/mod.rs index 57f6b91ff..94c308f39 100644 --- a/server/lib/src/valueset/mod.rs +++ b/server/lib/src/valueset/mod.rs @@ -3,46 +3,22 @@ use std::collections::{BTreeMap, BTreeSet}; use compact_jwt::JwsSigner; use dyn_clone::DynClone; use hashbrown::HashSet; -use kanidm_proto::v1::Filter as ProtoFilter; -use kanidm_proto::v1::UiHint; use smolset::SmolSet; use time::OffsetDateTime; // use std::fmt::Debug; use webauthn_rs::prelude::DeviceKey as DeviceKeyV4; use webauthn_rs::prelude::Passkey as PasskeyV4; +use kanidm_proto::v1::Filter as ProtoFilter; +use kanidm_proto::v1::UiHint; + use crate::be::dbvalue::DbValueSetV2; use crate::credential::{totp::Totp, Credential}; use crate::prelude::*; use crate::repl::{cid::Cid, proto::ReplAttrV1}; use crate::schema::SchemaAttribute; use crate::value::{Address, ApiToken, IntentTokenState, Oauth2Session, Session}; - -mod address; -mod binary; -mod bool; -mod cid; -mod cred; -mod datetime; -mod iname; -mod index; -mod iutf8; -mod json; -mod jws; -mod nsuniqueid; -mod oauth; -mod restricted; -mod secret; -mod session; -mod spn; -mod ssh; -mod syntax; -mod totp; -mod uihint; -mod uint32; -mod url; -mod utf8; -mod uuid; +use crate::valueset::auditlogstring::ValueSetAuditLogString; pub use self::address::{ValueSetAddress, ValueSetEmailAddress}; pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary}; @@ -70,6 +46,33 @@ pub use self::url::ValueSetUrl; pub use self::utf8::ValueSetUtf8; pub use self::uuid::{ValueSetRefer, ValueSetUuid}; +mod address; +mod auditlogstring; +mod binary; +mod bool; +mod cid; +mod cred; +mod datetime; +mod iname; +mod index; +mod iutf8; +mod json; +mod jws; +mod nsuniqueid; +mod oauth; +mod restricted; +mod secret; +mod session; +mod spn; +mod ssh; +mod syntax; +mod totp; +mod uihint; +mod uint32; +mod url; +mod utf8; +mod uuid; + pub type ValueSet = Box; dyn_clone::clone_trait_object!(ValueSetT); @@ -532,6 +535,10 @@ pub trait ValueSetT: std::fmt::Debug + DynClone { debug_assert!(false); None } + fn as_audit_log_string(&self) -> Option<&SmolSet<[(Cid, String); 8]>> { + debug_assert!(false); + None + } fn repl_merge_valueset( &self, @@ -607,6 +614,7 @@ pub fn from_result_value_iter( Value::IntentToken(u, s) => ValueSetIntentToken::new(u, s), Value::EmailAddress(a, _) => ValueSetEmailAddress::new(a), Value::UiHint(u) => ValueSetUiHint::new(u), + Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)), Value::PhoneNumber(_, _) | Value::Passkey(_, _, _) | Value::DeviceKey(_, _, _) @@ -673,6 +681,7 @@ pub fn from_value_iter(mut iter: impl Iterator) -> Result ValueSetOauth2Session::new(u, m), Value::UiHint(u) => ValueSetUiHint::new(u), Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t), + Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)), Value::PhoneNumber(_, _) => { debug_assert!(false); return Err(OperationError::InvalidValueState); @@ -722,6 +731,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result ValueSetJwsKeyEs256::from_dbvs2(&set), DbValueSetV2::UiHint(set) => ValueSetUiHint::from_dbvs2(set), DbValueSetV2::TotpSecret(set) => ValueSetTotpSecret::from_dbvs2(set), + DbValueSetV2::AuditLogString(set) => ValueSetAuditLogString::from_dbvs2(set), DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => { debug_assert!(false); Err(OperationError::InvalidValueState) @@ -768,5 +778,6 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result { ReplAttrV1::Session { set } => ValueSetSession::from_repl_v1(set), ReplAttrV1::ApiToken { set } => ValueSetApiToken::from_repl_v1(set), ReplAttrV1::TotpSecret { set } => ValueSetTotpSecret::from_repl_v1(set), + ReplAttrV1::AuditLogString { set } => ValueSetAuditLogString::from_repl_v1(set), } }