Name change history (#1727)

This commit is contained in:
Sebastiano Tocci 2023-06-28 10:34:44 +02:00 committed by GitHub
parent b752ab65b8
commit 9a3c12a79d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 551 additions and 51 deletions

View file

@ -25,7 +25,7 @@
- Yuxuan Lu (leoleoasd) - Yuxuan Lu (leoleoasd)
- h7x4 - h7x4
- Pi-Cla - Pi-Cla
- Sebastiano Tocci(Seba-T) - Sebastiano Tocci (Seba-T)
- Minh Phan (MinhPhan8803) - Minh Phan (MinhPhan8803)
- Kenton Groombridge (0xC0ncord) - Kenton Groombridge (0xC0ncord)
- Martin Weinelt (hexa) - Martin Weinelt (hexa)

View file

@ -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 An additional complexity is that both servers must be able to resolve this
conflict in isolation, without further communication. All servers must arrive 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. be the same to all members of the replication topology.
As a result, almost all facets of replication are designed around the possible As a result, almost all facets of replication are designed around the possible

View file

@ -11,6 +11,7 @@ use webauthn_rs::prelude::{
use webauthn_rs_core::proto::{COSEKey, UserVerificationPolicy}; use webauthn_rs_core::proto::{COSEKey, UserVerificationPolicy};
// Re-export this as though it was here. // Re-export this as though it was here.
use crate::repl::cid::Cid;
pub use kanidm_lib_crypto::DbPasswordV1; pub use kanidm_lib_crypto::DbPasswordV1;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -608,6 +609,8 @@ pub enum DbValueSetV2 {
TotpSecret(Vec<(String, DbTotpV1)>), TotpSecret(Vec<(String, DbTotpV1)>),
#[serde(rename = "AT")] #[serde(rename = "AT")]
ApiToken(Vec<DbValueApiToken>), ApiToken(Vec<DbValueApiToken>),
#[serde(rename = "SA")]
AuditLogString(Vec<(Cid, String)>),
} }
impl DbValueSetV2 { impl DbValueSetV2 {
@ -650,6 +653,7 @@ impl DbValueSetV2 {
DbValueSetV2::JwsKeyRs256(set) => set.len(), DbValueSetV2::JwsKeyRs256(set) => set.len(),
DbValueSetV2::UiHint(set) => set.len(), DbValueSetV2::UiHint(set) => set.len(),
DbValueSetV2::TotpSecret(set) => set.len(), DbValueSetV2::TotpSecret(set) => set.len(),
DbValueSetV2::AuditLogString(set) => set.len(),
} }
} }

View file

@ -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#"{ pub const JSON_SCHEMA_ATTR_RADIUS_SECRET: &str = r#"{
"attrs": { "attrs": {
"class": [ "class": [
@ -1578,7 +1613,8 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#"
"oauth2_consent_scope_map", "oauth2_consent_scope_map",
"user_auth_token_session", "user_auth_token_session",
"oauth2_session", "oauth2_session",
"description" "description",
"name_history"
], ],
"systemmust": [ "systemmust": [
"displayname", "displayname",

View file

@ -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 = pub const _UUID_SCHEMA_ATTR_DOMAIN_LDAP_BASEDN: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000131"); uuid!("00000000-0000-0000-0000-ffff00000131");
pub const UUID_SCHEMA_ATTR_DYNMEMBER: Uuid = uuid!("00000000-0000-0000-0000-ffff00000132"); 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 // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -12,7 +12,7 @@ use crate::prelude::*;
// This module has some special properties around it's operation, namely that it // 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 // 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 // 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 // pre_create_transform step, where every other SHOULD use the post_* hooks for all
// validation operations. // validation operations.

View file

@ -19,6 +19,7 @@ pub(crate) mod dyngroup;
mod gidnumber; mod gidnumber;
mod jwskeygen; mod jwskeygen;
mod memberof; mod memberof;
mod namehistory;
mod protected; mod protected;
mod refint; mod refint;
mod session; mod session;
@ -207,6 +208,7 @@ impl Plugins {
.and_then(|_| gidnumber::GidNumber::pre_create_transform(qs, cand, ce)) .and_then(|_| gidnumber::GidNumber::pre_create_transform(qs, cand, ce))
.and_then(|_| domain::Domain::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(|_| spn::Spn::pre_create_transform(qs, cand, ce))
.and_then(|_| namehistory::NameHistory::pre_create_transform(qs, cand, ce))
// Should always be last // Should always be last
.and_then(|_| attrunique::AttrUnique::pre_create_transform(qs, cand, ce)) .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(|_| domain::Domain::pre_modify(qs, pre_cand, cand, me))
.and_then(|_| spn::Spn::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(|_| 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 // attr unique should always be last
.and_then(|_| attrunique::AttrUnique::pre_modify(qs, pre_cand, cand, me)) .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(|_| domain::Domain::pre_batch_modify(qs, pre_cand, cand, me))
.and_then(|_| spn::Spn::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(|_| 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 // attr unique should always be last
.and_then(|_| attrunique::AttrUnique::pre_batch_modify(qs, pre_cand, cand, me)) .and_then(|_| attrunique::AttrUnique::pre_batch_modify(qs, pre_cand, cand, me))
} }

View file

@ -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<VALUE, STATE>(entry: &mut Entry<VALUE, STATE>) -> 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<EntrySealedCommitted>],
cand: &mut Vec<EntryInvalidCommitted>,
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<EntryInvalidNew>,
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<EntryInvalidNew>,
_ce: &CreateEvent,
) -> Result<(), OperationError> {
Self::handle_name_creation(cand, qs.get_txn_cid())
}
fn pre_modify(
qs: &mut QueryServerWriteTransaction,
pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<EntryInvalidCommitted>,
_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<EntrySealedCommitted>],
cand: &mut Vec<EntryInvalidCommitted>,
_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<Cid> = 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"))
);
}
);
}
}

View file

@ -375,6 +375,9 @@ pub enum ReplAttrV1 {
TotpSecret { TotpSecret {
set: Vec<(String, ReplTotpV1)>, set: Vec<(String, ReplTotpV1)>,
}, },
AuditLogString {
set: Vec<(Cid, String)>,
},
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]

View file

@ -213,6 +213,7 @@ impl SchemaAttribute {
SyntaxType::UiHint => matches!(v, PartialValue::UiHint(_)), SyntaxType::UiHint => matches!(v, PartialValue::UiHint(_)),
// Comparing on the label. // Comparing on the label.
SyntaxType::TotpSecret => matches!(v, PartialValue::Utf8(_)), SyntaxType::TotpSecret => matches!(v, PartialValue::Utf8(_)),
SyntaxType::AuditLogString => matches!(v, PartialValue::Utf8(_)),
}; };
if r { if r {
Ok(()) Ok(())
@ -262,6 +263,7 @@ impl SchemaAttribute {
SyntaxType::JwsKeyRs256 => matches!(v, Value::JwsKeyRs256(_)), SyntaxType::JwsKeyRs256 => matches!(v, Value::JwsKeyRs256(_)),
SyntaxType::UiHint => matches!(v, Value::UiHint(_)), SyntaxType::UiHint => matches!(v, Value::UiHint(_)),
SyntaxType::TotpSecret => matches!(v, Value::TotpSecret(_, _)), SyntaxType::TotpSecret => matches!(v, Value::TotpSecret(_, _)),
SyntaxType::AuditLogString => matches!(v, Value::Utf8(_)),
}; };
if r { if r {
Ok(()) Ok(())

View file

@ -200,6 +200,11 @@ mod tests {
e.add_ava("directmemberof", Value::Refer(UUID_IDM_ALL_PERSONS)); e.add_ava("directmemberof", Value::Refer(UUID_IDM_ALL_PERSONS));
e.add_ava("memberof", Value::Refer(UUID_IDM_ALL_ACCOUNTS)); e.add_ava("memberof", Value::Refer(UUID_IDM_ALL_ACCOUNTS));
e.add_ava("directmemberof", 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())] }; let expected = unsafe { vec![Arc::new(e.into_sealed_committed())] };

View file

@ -423,6 +423,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
let idm_schema: Vec<&str> = vec![ let idm_schema: Vec<&str> = vec![
JSON_SCHEMA_ATTR_DISPLAYNAME, JSON_SCHEMA_ATTR_DISPLAYNAME,
JSON_SCHEMA_ATTR_LEGALNAME, JSON_SCHEMA_ATTR_LEGALNAME,
JSON_SCHEMA_ATTR_NAME_HISTORY,
JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_MAIL,
JSON_SCHEMA_ATTR_SSH_PUBLICKEY, JSON_SCHEMA_ATTR_SSH_PUBLICKEY,
JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL,

View file

@ -4,28 +4,20 @@
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use crate::prelude::*;
use concread::arcache::{ARCache, ARCacheBuilder, ARCacheReadTxn}; use concread::arcache::{ARCache, ARCacheBuilder, ARCacheReadTxn};
use concread::cowcell::*; use concread::cowcell::*;
use hashbrown::HashSet; use hashbrown::HashSet;
use kanidm_proto::v1::{ConsistencyError, UiHint};
use tokio::sync::{Semaphore, SemaphorePermit}; use tokio::sync::{Semaphore, SemaphorePermit};
use tracing::trace; use tracing::trace;
use self::access::{ use kanidm_proto::v1::{ConsistencyError, UiHint};
profiles::{
AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch,
},
AccessControls, AccessControlsReadTransaction, AccessControlsTransaction,
AccessControlsWriteTransaction,
};
use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction}; use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction};
// We use so many, we just import them all ... // We use so many, we just import them all ...
use crate::filter::{Filter, FilterInvalid, FilterValid, FilterValidResolved}; use crate::filter::{Filter, FilterInvalid, FilterValid, FilterValidResolved};
use crate::plugins::dyngroup::{DynGroup, DynGroupCache}; use crate::plugins::dyngroup::{DynGroup, DynGroupCache};
use crate::plugins::Plugins; use crate::plugins::Plugins;
use crate::prelude::*;
use crate::repl::cid::Cid; use crate::repl::cid::Cid;
use crate::repl::proto::ReplRuvRange; use crate::repl::proto::ReplRuvRange;
use crate::repl::ruv::ReplicationUpdateVectorTransaction; use crate::repl::ruv::ReplicationUpdateVectorTransaction;
@ -36,6 +28,14 @@ use crate::schema::{
use crate::value::EXTRACT_VAL_DN; use crate::value::EXTRACT_VAL_DN;
use crate::valueset::uuid_to_proto_string; use crate::valueset::uuid_to_proto_string;
use self::access::{
profiles::{
AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch,
},
AccessControls, AccessControlsReadTransaction, AccessControlsTransaction,
AccessControlsWriteTransaction,
};
pub mod access; pub mod access;
pub mod batch_modify; pub mod batch_modify;
pub mod create; pub mod create;
@ -543,6 +543,7 @@ pub trait QueryServerTransaction<'a> {
.map(Value::UiHint) .map(Value::UiHint)
.map_err(|()| OperationError::InvalidAttribute("Invalid uihint syntax".to_string())), .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::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 => { None => {
@ -649,6 +650,7 @@ pub trait QueryServerTransaction<'a> {
.map_err(|()| { .map_err(|()| {
OperationError::InvalidAttribute("Invalid uihint syntax".to_string()) OperationError::InvalidAttribute("Invalid uihint syntax".to_string())
}), }),
SyntaxType::AuditLogString => Ok(PartialValue::new_utf8s(value)),
} }
} }
None => { None => {
@ -1540,11 +1542,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
.and_then(|_| accesscontrols.commit()) .and_then(|_| accesscontrols.commit())
.and_then(|_| be_txn.commit()) .and_then(|_| be_txn.commit())
} }
pub(crate) fn get_txn_cid(&self) -> &Cid {
&self.cid
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::prelude::*; use crate::prelude::*;
#[qs_test] #[qs_test]

View file

@ -3,8 +3,6 @@
//! typed values, allows their comparison, filtering and more. It also has the code for serialising //! 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). //! 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::collections::BTreeSet;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt; use std::fmt;
@ -12,15 +10,10 @@ use std::fmt::Formatter;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use crate::valueset::uuid_to_proto_string;
#[cfg(test)] #[cfg(test)]
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use compact_jwt::JwsSigner; use compact_jwt::JwsSigner;
use hashbrown::HashSet; 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 num_enum::TryFromPrimitive;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -30,10 +23,17 @@ use url::Url;
use uuid::Uuid; use uuid::Uuid;
use webauthn_rs::prelude::{DeviceKey as DeviceKeyV4, Passkey as PasskeyV4}; 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::be::dbentry::DbIdentSpn;
use crate::credential::{totp::Totp, Credential}; use crate::credential::{totp::Totp, Credential};
use crate::prelude::*;
use crate::repl::cid::Cid; use crate::repl::cid::Cid;
use crate::server::identity::IdentityId; use crate::server::identity::IdentityId;
use crate::valueset::uuid_to_proto_string;
lazy_static! { lazy_static! {
pub static ref SPN_RE: Regex = { pub static ref SPN_RE: Regex = {
@ -239,6 +239,7 @@ pub enum SyntaxType {
UiHint = 29, UiHint = 29,
TotpSecret = 30, TotpSecret = 30,
ApiToken = 31, ApiToken = 31,
AuditLogString = 32,
} }
impl TryFrom<&str> for SyntaxType { impl TryFrom<&str> for SyntaxType {
@ -280,6 +281,7 @@ impl TryFrom<&str> for SyntaxType {
"UIHINT" => Ok(SyntaxType::UiHint), "UIHINT" => Ok(SyntaxType::UiHint),
"TOTPSECRET" => Ok(SyntaxType::TotpSecret), "TOTPSECRET" => Ok(SyntaxType::TotpSecret),
"APITOKEN" => Ok(SyntaxType::ApiToken), "APITOKEN" => Ok(SyntaxType::ApiToken),
"AUDIT_LOG_STRING" => Ok(SyntaxType::AuditLogString),
_ => Err(()), _ => Err(()),
} }
} }
@ -320,6 +322,7 @@ impl fmt::Display for SyntaxType {
SyntaxType::UiHint => "UIHINT", SyntaxType::UiHint => "UIHINT",
SyntaxType::TotpSecret => "TOTPSECRET", SyntaxType::TotpSecret => "TOTPSECRET",
SyntaxType::ApiToken => "APITOKEN", SyntaxType::ApiToken => "APITOKEN",
SyntaxType::AuditLogString => "AUDIT_LOG_STRING",
}) })
} }
} }
@ -887,6 +890,7 @@ pub enum Value {
UiHint(UiHint), UiHint(UiHint),
TotpSecret(String, Totp), TotpSecret(String, Totp),
AuditLogString(Cid, String),
} }
impl PartialEq for Value { impl PartialEq for Value {
@ -1089,6 +1093,10 @@ impl Value {
bool::from_str(s).map(Value::Bool).ok() bool::from_str(s).map(Value::Bool).ok()
} }
pub fn new_audit_log_string(e: (Cid, String)) -> Option<Self> {
Some(Value::AuditLogString(e.0, e.1))
}
#[inline] #[inline]
pub fn is_bool(&self) -> bool { pub fn is_bool(&self) -> bool {
matches!(self, Value::Bool(_)) matches!(self, Value::Bool(_))
@ -1653,7 +1661,9 @@ impl Value {
Value::ApiToken(_, at) => { Value::ApiToken(_, at) => {
Value::validate_str_escapes(&at.label) && Value::validate_singleline(&at.label) 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. // These have stricter validators so not needed.
Value::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s), Value::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s),
Value::DateTime(odt) => odt.offset() == time::UtcOffset::UTC, Value::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,

View file

@ -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<Self> {
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<AuditLogStringType>) -> Result<ValueSet, OperationError> {
let set = data.into_iter().collect();
Ok(Box::new(ValueSetAuditLogString { set }))
}
pub fn from_repl_v1(data: &[AuditLogStringType]) -> Result<ValueSet, OperationError> {
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<bool, OperationError> {
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<String> {
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<dyn Iterator<Item = String> + '_> {
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<dyn Iterator<Item = PartialValue> + '_> {
Box::new(self.set.iter().map(|(_, s)| PartialValue::Utf8(s.clone())))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
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)
}
}

View file

@ -3,46 +3,22 @@ use std::collections::{BTreeMap, BTreeSet};
use compact_jwt::JwsSigner; use compact_jwt::JwsSigner;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use hashbrown::HashSet; use hashbrown::HashSet;
use kanidm_proto::v1::Filter as ProtoFilter;
use kanidm_proto::v1::UiHint;
use smolset::SmolSet; use smolset::SmolSet;
use time::OffsetDateTime; use time::OffsetDateTime;
// use std::fmt::Debug; // use std::fmt::Debug;
use webauthn_rs::prelude::DeviceKey as DeviceKeyV4; use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
use webauthn_rs::prelude::Passkey as PasskeyV4; 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::be::dbvalue::DbValueSetV2;
use crate::credential::{totp::Totp, Credential}; use crate::credential::{totp::Totp, Credential};
use crate::prelude::*; use crate::prelude::*;
use crate::repl::{cid::Cid, proto::ReplAttrV1}; use crate::repl::{cid::Cid, proto::ReplAttrV1};
use crate::schema::SchemaAttribute; use crate::schema::SchemaAttribute;
use crate::value::{Address, ApiToken, IntentTokenState, Oauth2Session, Session}; use crate::value::{Address, ApiToken, IntentTokenState, Oauth2Session, Session};
use crate::valueset::auditlogstring::ValueSetAuditLogString;
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;
pub use self::address::{ValueSetAddress, ValueSetEmailAddress}; pub use self::address::{ValueSetAddress, ValueSetEmailAddress};
pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary}; pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary};
@ -70,6 +46,33 @@ pub use self::url::ValueSetUrl;
pub use self::utf8::ValueSetUtf8; pub use self::utf8::ValueSetUtf8;
pub use self::uuid::{ValueSetRefer, ValueSetUuid}; 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 ValueSetT + Send + Sync + 'static>; pub type ValueSet = Box<dyn ValueSetT + Send + Sync + 'static>;
dyn_clone::clone_trait_object!(ValueSetT); dyn_clone::clone_trait_object!(ValueSetT);
@ -532,6 +535,10 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
debug_assert!(false); debug_assert!(false);
None None
} }
fn as_audit_log_string(&self) -> Option<&SmolSet<[(Cid, String); 8]>> {
debug_assert!(false);
None
}
fn repl_merge_valueset( fn repl_merge_valueset(
&self, &self,
@ -607,6 +614,7 @@ pub fn from_result_value_iter(
Value::IntentToken(u, s) => ValueSetIntentToken::new(u, s), Value::IntentToken(u, s) => ValueSetIntentToken::new(u, s),
Value::EmailAddress(a, _) => ValueSetEmailAddress::new(a), Value::EmailAddress(a, _) => ValueSetEmailAddress::new(a),
Value::UiHint(u) => ValueSetUiHint::new(u), Value::UiHint(u) => ValueSetUiHint::new(u),
Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)),
Value::PhoneNumber(_, _) Value::PhoneNumber(_, _)
| Value::Passkey(_, _, _) | Value::Passkey(_, _, _)
| Value::DeviceKey(_, _, _) | Value::DeviceKey(_, _, _)
@ -673,6 +681,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
Value::Oauth2Session(u, m) => ValueSetOauth2Session::new(u, m), Value::Oauth2Session(u, m) => ValueSetOauth2Session::new(u, m),
Value::UiHint(u) => ValueSetUiHint::new(u), Value::UiHint(u) => ValueSetUiHint::new(u),
Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t), Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t),
Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)),
Value::PhoneNumber(_, _) => { Value::PhoneNumber(_, _) => {
debug_assert!(false); debug_assert!(false);
return Err(OperationError::InvalidValueState); return Err(OperationError::InvalidValueState);
@ -722,6 +731,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set), DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
DbValueSetV2::UiHint(set) => ValueSetUiHint::from_dbvs2(set), DbValueSetV2::UiHint(set) => ValueSetUiHint::from_dbvs2(set),
DbValueSetV2::TotpSecret(set) => ValueSetTotpSecret::from_dbvs2(set), DbValueSetV2::TotpSecret(set) => ValueSetTotpSecret::from_dbvs2(set),
DbValueSetV2::AuditLogString(set) => ValueSetAuditLogString::from_dbvs2(set),
DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => { DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => {
debug_assert!(false); debug_assert!(false);
Err(OperationError::InvalidValueState) Err(OperationError::InvalidValueState)
@ -768,5 +778,6 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
ReplAttrV1::Session { set } => ValueSetSession::from_repl_v1(set), ReplAttrV1::Session { set } => ValueSetSession::from_repl_v1(set),
ReplAttrV1::ApiToken { set } => ValueSetApiToken::from_repl_v1(set), ReplAttrV1::ApiToken { set } => ValueSetApiToken::from_repl_v1(set),
ReplAttrV1::TotpSecret { set } => ValueSetTotpSecret::from_repl_v1(set), ReplAttrV1::TotpSecret { set } => ValueSetTotpSecret::from_repl_v1(set),
ReplAttrV1::AuditLogString { set } => ValueSetAuditLogString::from_repl_v1(set),
} }
} }