mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Name change history (#1727)
This commit is contained in:
parent
b752ab65b8
commit
9a3c12a79d
|
@ -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
|
||||
|
|
|
@ -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<DbValueApiToken>),
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
267
server/lib/src/plugins/namehistory.rs
Normal file
267
server/lib/src/plugins/namehistory.rs
Normal 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"))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -375,6 +375,9 @@ pub enum ReplAttrV1 {
|
|||
TotpSecret {
|
||||
set: Vec<(String, ReplTotpV1)>,
|
||||
},
|
||||
AuditLogString {
|
||||
set: Vec<(Cid, String)>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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())] };
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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<Self> {
|
||||
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,
|
||||
|
|
152
server/lib/src/valueset/auditlogstring.rs
Normal file
152
server/lib/src/valueset/auditlogstring.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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 ValueSetT + Send + Sync + 'static>;
|
||||
|
||||
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<Item = Value>) -> Result<ValueSet
|
|||
Value::Oauth2Session(u, m) => 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<ValueSet, OperationErro
|
|||
DbValueSetV2::JwsKeyRs256(set) => 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<ValueSet, OperationError> {
|
|||
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),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue