20220908 dynamic groups ()

This commit is contained in:
Firstyear 2022-09-11 12:23:57 +10:00 committed by GitHub
parent bba5bd1a42
commit d66edb829c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1383 additions and 240 deletions

42
Cargo.lock generated
View file

@ -744,20 +744,7 @@ dependencies = [
"serde_json",
"tracing",
"url",
"uuid 1.1.2",
]
[[package]]
name = "compiled-uuid"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18c9f404cbbb38aeaaa6a0cfe4039df817377926183d278098cdfe96dd015610"
dependencies = [
"proc-macro2",
"quote",
"syn",
"thiserror",
"uuid 0.8.2",
"uuid",
]
[[package]]
@ -2123,7 +2110,6 @@ dependencies = [
"base64urlsafedata",
"chrono",
"compact_jwt",
"compiled-uuid",
"concread",
"criterion",
"dyn-clone",
@ -2164,7 +2150,7 @@ dependencies = [
"url",
"urlencoding",
"users",
"uuid 1.1.2",
"uuid",
"validator",
"webauthn-authenticator-rs",
"webauthn-rs",
@ -2185,7 +2171,7 @@ dependencies = [
"toml",
"tracing",
"url",
"uuid 1.1.2",
"uuid",
"webauthn-rs-proto",
]
@ -2201,7 +2187,7 @@ dependencies = [
"time 0.2.27",
"url",
"urlencoding",
"uuid 1.1.2",
"uuid",
"webauthn-rs-proto",
]
@ -2227,7 +2213,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"url",
"uuid 1.1.2",
"uuid",
"webauthn-authenticator-rs",
"zxcvbn",
]
@ -2274,7 +2260,7 @@ dependencies = [
"qrcode",
"serde",
"serde_json",
"uuid 1.1.2",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@ -2809,7 +2795,7 @@ dependencies = [
"toml",
"tracing",
"tracing-subscriber",
"uuid 1.1.2",
"uuid",
]
[[package]]
@ -3499,7 +3485,7 @@ dependencies = [
"tokio-util",
"tracing",
"url",
"uuid 1.1.2",
"uuid",
"webauthn-authenticator-rs",
]
@ -4298,7 +4284,7 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
"uuid 1.1.2",
"uuid",
]
[[package]]
@ -4414,12 +4400,6 @@ dependencies = [
"log",
]
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "uuid"
version = "1.1.2"
@ -4657,7 +4637,7 @@ dependencies = [
"serde",
"tracing",
"url",
"uuid 1.1.2",
"uuid",
"webauthn-rs-core",
]
@ -4680,7 +4660,7 @@ dependencies = [
"thiserror",
"tracing",
"url",
"uuid 1.1.2",
"uuid",
"webauthn-rs-proto",
"x509-parser",
]

View file

@ -21,7 +21,6 @@ base64 = "^0.13.0"
base64urlsafedata = "0.1.0"
chrono = "^0.4.20"
compact_jwt = "^0.2.3"
compiled-uuid = "0.1.2"
concread = "^0.3.7"
dyn-clone = "^1.0.9"
fernet = { version = "^0.1.4", features = ["fernet_danger_timestamps"] }

View file

@ -1524,9 +1524,9 @@ mod tests {
};
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, SearchEvent};
use crate::prelude::*;
use compiled_uuid::uuid;
use std::collections::BTreeSet;
use std::sync::Arc;
use uuid::uuid;
macro_rules! acp_from_entry_err {
(
@ -1548,8 +1548,8 @@ mod tests {
$e:expr,
$type:ty
) => {{
let e1: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str($e);
let ev1 = unsafe { e1.into_sealed_committed() };
// let e1: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str($e);
let ev1 = unsafe { $e.into_sealed_committed() };
let r1 = <$type>::try_from($qs, &ev1);
assert!(r1.is_ok());
@ -1611,19 +1611,23 @@ mod tests {
// "\"Self\""
acp_from_entry_ok!(
&mut qs_write,
r#"{
"attrs": {
"class": ["object", "access_control_profile"],
"name": ["acp_valid"],
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
"acp_receiver": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_targetscope": [
"{\"eq\":[\"name\",\"a\"]}"
]
}
}"#,
entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("access_control_profile")),
("name", Value::new_iname("acp_valid")),
(
"uuid",
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
),
(
"acp_receiver",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(
"acp_targetscope",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
)
),
AccessControlProfile
);
})
@ -1654,19 +1658,24 @@ mod tests {
acp_from_entry_ok!(
&mut qs_write,
r#"{
"attrs": {
"class": ["object", "access_control_profile", "access_control_delete"],
"name": ["acp_valid"],
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
"acp_receiver": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_targetscope": [
"{\"eq\":[\"name\",\"a\"]}"
]
}
}"#,
entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("access_control_profile")),
("class", Value::new_class("access_control_delete")),
("name", Value::new_iname("acp_valid")),
(
"uuid",
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
),
(
"acp_receiver",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(
"acp_targetscope",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
)
),
AccessControlDelete
);
})
@ -1740,20 +1749,26 @@ mod tests {
// All good!
acp_from_entry_ok!(
&mut qs_write,
r#"{
"attrs": {
"class": ["object", "access_control_profile", "access_control_search"],
"name": ["acp_valid"],
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
"acp_receiver": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_targetscope": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_search_attr": ["name", "class"]
}
}"#,
entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("access_control_profile")),
("class", Value::new_class("access_control_search")),
("name", Value::new_iname("acp_valid")),
(
"uuid",
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
),
(
"acp_receiver",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(
"acp_targetscope",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
("acp_search_attr", Value::new_iutf8("name")),
("acp_search_attr", Value::new_iutf8("class"))
),
AccessControlSearch
);
})
@ -1788,40 +1803,50 @@ mod tests {
acp_from_entry_ok!(
&mut qs_write,
r#"{
"attrs": {
"class": ["object", "access_control_profile", "access_control_modify"],
"name": ["acp_valid"],
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
"acp_receiver": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_targetscope": [
"{\"eq\":[\"name\",\"a\"]}"
]
}
}"#,
entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("access_control_profile")),
("class", Value::new_class("access_control_modify")),
("name", Value::new_iname("acp_valid")),
(
"uuid",
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
),
(
"acp_receiver",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(
"acp_targetscope",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
)
),
AccessControlModify
);
acp_from_entry_ok!(
&mut qs_write,
r#"{
"attrs": {
"class": ["object", "access_control_profile", "access_control_modify"],
"name": ["acp_valid"],
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
"acp_receiver": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_targetscope": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_modify_removedattr": ["name"],
"acp_modify_presentattr": ["name"],
"acp_modify_class": ["object"]
}
}"#,
entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("access_control_profile")),
("class", Value::new_class("access_control_modify")),
("name", Value::new_iname("acp_valid")),
(
"uuid",
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
),
(
"acp_receiver",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(
"acp_targetscope",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
("acp_modify_removedattr", Value::new_iutf8("name")),
("acp_modify_presentattr", Value::new_iutf8("name")),
("acp_modify_class", Value::new_iutf8("object"))
),
AccessControlModify
);
})
@ -1855,39 +1880,49 @@ mod tests {
acp_from_entry_ok!(
&mut qs_write,
r#"{
"attrs": {
"class": ["object", "access_control_profile", "access_control_create"],
"name": ["acp_valid"],
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
"acp_receiver": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_targetscope": [
"{\"eq\":[\"name\",\"a\"]}"
]
}
}"#,
entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("access_control_profile")),
("class", Value::new_class("access_control_create")),
("name", Value::new_iname("acp_valid")),
(
"uuid",
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
),
(
"acp_receiver",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(
"acp_targetscope",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
)
),
AccessControlCreate
);
acp_from_entry_ok!(
&mut qs_write,
r#"{
"attrs": {
"class": ["object", "access_control_profile", "access_control_create"],
"name": ["acp_valid"],
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
"acp_receiver": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_targetscope": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_create_class": ["object"],
"acp_create_attr": ["name"]
}
}"#,
entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("access_control_profile")),
("class", Value::new_class("access_control_create")),
("name", Value::new_iname("acp_valid")),
(
"uuid",
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
),
(
"acp_receiver",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(
"acp_targetscope",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
("acp_create_attr", Value::new_iutf8("name")),
("acp_create_class", Value::new_iutf8("object"))
),
AccessControlCreate
);
})
@ -1902,36 +1937,37 @@ mod tests {
// over a single scope.
let mut qs_write = qs.write(duration_from_epoch_now());
let e: &str = r#"{
"attrs": {
"class": [
"object",
"access_control_profile",
"access_control_create",
"access_control_delete",
"access_control_modify",
"access_control_search"
],
"name": ["acp_valid"],
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
"acp_receiver": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_targetscope": [
"{\"eq\":[\"name\",\"a\"]}"
],
"acp_search_attr": ["name"],
"acp_create_class": ["object"],
"acp_create_attr": ["name"],
"acp_modify_removedattr": ["name"],
"acp_modify_presentattr": ["name"],
"acp_modify_class": ["object"]
}
}"#;
let e = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("access_control_profile")),
("class", Value::new_class("access_control_create")),
("class", Value::new_class("access_control_delete")),
("class", Value::new_class("access_control_modify")),
("class", Value::new_class("access_control_search")),
("name", Value::new_iname("acp_valid")),
(
"uuid",
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
),
(
"acp_receiver",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(
"acp_targetscope",
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
("acp_search_attr", Value::new_iutf8("name")),
("acp_create_class", Value::new_iutf8("class")),
("acp_create_attr", Value::new_iutf8("name")),
("acp_modify_removedattr", Value::new_iutf8("name")),
("acp_modify_presentattr", Value::new_iutf8("name")),
("acp_modify_class", Value::new_iutf8("object"))
);
acp_from_entry_ok!(&mut qs_write, e, AccessControlCreate);
acp_from_entry_ok!(&mut qs_write, e, AccessControlDelete);
acp_from_entry_ok!(&mut qs_write, e, AccessControlModify);
acp_from_entry_ok!(&mut qs_write, e.clone(), AccessControlCreate);
acp_from_entry_ok!(&mut qs_write, e.clone(), AccessControlDelete);
acp_from_entry_ok!(&mut qs_write, e.clone(), AccessControlModify);
acp_from_entry_ok!(&mut qs_write, e, AccessControlSearch);
})
}

View file

@ -415,6 +415,18 @@ pub const JSON_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV: &str = r#"{
}
}"#;
pub const JSON_IDM_ALL_PERSONS: &str = r#"{
"attrs": {
"class": ["dyngroup", "group", "object"],
"name": ["idm_all_persons"],
"uuid": ["00000000-0000-0000-0000-000000000035"],
"description": ["Builtin IDM dynamic group containing all persons"],
"dyngroup_filter": [
"{\"eq\":[\"class\",\"person\"]}"
]
}
}"#;
/// This must be the last group to init to include the UUID of the other high priv groups.
pub const JSON_IDM_HIGH_PRIVILEGE_V1: &str = r#"{
"attrs": {

View file

@ -990,6 +990,34 @@ pub const JSON_SCHEMA_ATTR_DEVICEKEYS: &str = r#"{
}
}"#;
pub const JSON_SCHEMA_ATTR_DYNGROUP_FILTER: &str = r#"{
"attrs": {
"class": [
"object",
"system",
"attributetype"
],
"description": [
"A filter describing the set of entries to add to a dynamic group"
],
"unique": [
"false"
],
"multivalue": [
"false"
],
"attributename": [
"dyngroup_filter"
],
"syntax": [
"JSON_FILTER"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000108"
]
}
}"#;
// === classes ===
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
@ -1078,6 +1106,33 @@ pub const JSON_SCHEMA_CLASS_GROUP: &str = r#"
}
"#;
pub const JSON_SCHEMA_CLASS_DYNGROUP: &str = r#"
{
"attrs": {
"class": [
"object",
"system",
"classtype"
],
"description": [
"Object representation of a dynamic group"
],
"classname": [
"dyngroup"
],
"systemmust": [
"dyngroup_filter"
],
"systemsupplements": [
"group"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000107"
]
}
}
"#;
pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#"
{
"attrs": {

View file

@ -1,7 +1,6 @@
#![allow(clippy::unwrap_used)]
use compiled_uuid::uuid;
use uuid::Uuid;
use uuid::{uuid, Uuid};
// Built in group and account ranges.
pub const STR_UUID_ADMIN: &str = "00000000-0000-0000-0000-000000000000";
@ -47,6 +46,8 @@ pub const _UUID_IDM_PEOPLE_SELF_WRITE_MAIL_PRIV: Uuid =
pub const _UUID_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV: Uuid =
uuid!("00000000-0000-0000-0000-000000000034");
pub const UUID_IDM_ALL_PERSONS: Uuid = uuid!("00000000-0000-0000-0000-000000000035");
//
pub const _UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000");
@ -181,6 +182,8 @@ pub const UUID_SCHEMA_ATTR_SYSTEMEXCLUDES: Uuid = uuid!("00000000-0000-0000-0000
pub const UUID_SCHEMA_ATTR_EXCLUDES: Uuid = uuid!("00000000-0000-0000-0000-ffff00000104");
pub const UUID_SCHEMA_ATTR_SCOPE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000105");
pub const UUID_SCHEMA_CLASS_SERVICE_ACCOUNT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000106");
pub const _UUID_SCHEMA_CLASS_DYNGROUP: Uuid = uuid!("00000000-0000-0000-0000-ffff00000107");
pub const _UUID_SCHEMA_ATTR_DYNGROUP_FILTER: Uuid = uuid!("00000000-0000-0000-0000-ffff00000108");
// System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -2277,7 +2277,7 @@ where
/// Remove an attribute-value pair from this entry. If the ava doesn't exist, we
/// don't do anything else since we are asserting the abscence of a value.
fn remove_ava(&mut self, attr: &str, value: &PartialValue) {
pub(crate) fn remove_ava(&mut self, attr: &str, value: &PartialValue) {
self.valid
.eclog
.remove_ava_iter(&self.valid.cid, attr, std::iter::once(value.clone()));

View file

@ -1488,8 +1488,8 @@ mod tests {
use webauthn_authenticator_rs::{softpasskey::SoftPasskey, WebauthnAuthenticator};
use crate::idm::AuthState;
use compiled_uuid::uuid;
use kanidm_proto::v1::{AuthAllowed, AuthMech, CredentialDetailType};
use uuid::uuid;
use async_std::task;

View file

@ -1,7 +1,8 @@
//! The Kanidmd server library. This implements all of the internal components of the server
//! which is used to process authentication, store identities and enforce access controls.
#![deny(warnings)]
//#![deny(warnings)]
#![recursion_limit = "512"]
#![warn(unused_extern_crates)]
#![deny(clippy::todo)]
@ -64,7 +65,7 @@ pub mod config;
/// help make imports cleaner.
pub mod prelude {
pub use crate::utils::duration_from_epoch_now;
pub use kanidm_proto::v1::OperationError;
pub use kanidm_proto::v1::{ConsistencyError, OperationError};
pub use smartstring::alias::String as AttrString;
pub use url::Url;
pub use uuid::Uuid;

View file

@ -207,6 +207,7 @@ impl ModifyList<ModifyInvalid> {
})
}
#[cfg(test)]
pub(crate) unsafe fn into_valid(self) -> ModifyList<ModifyValid> {
ModifyList {
valid: ModifyValid,

View file

@ -113,6 +113,11 @@ impl Plugin for AttrUnique {
"plugin_attrunique"
}
#[instrument(
level = "debug",
name = "attrunique_pre_create_transform",
skip(qs, cand, _ce)
)]
fn pre_create_transform(
qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
@ -130,6 +135,7 @@ impl Plugin for AttrUnique {
r
}
#[instrument(level = "debug", name = "attrunique_pre_modify", skip(qs, cand, _me))]
fn pre_modify(
qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
@ -147,6 +153,7 @@ impl Plugin for AttrUnique {
r
}
#[instrument(level = "debug", name = "attrunique_verify", skip(qs))]
fn verify(qs: &QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
// Only check live entries, not recycled.
let filt_in = filter!(f_pres("class"));

View file

@ -28,14 +28,12 @@ impl Plugin for Base {
fn id() -> &'static str {
"plugin_base"
}
// Need to be given the backend(for testing ease)
// audit
// the mut set of entries to create
// the create event itself (immutable, for checking originals)
// contains who is creating them
// the schema of the running instance
// TODO: Can this be improved?
#[instrument(
level = "debug",
name = "base_pre_create_transform",
skip(qs, cand, ce)
)]
#[allow(clippy::cognitive_complexity)]
fn pre_create_transform(
qs: &QueryServerWriteTransaction,
@ -169,6 +167,7 @@ impl Plugin for Base {
Ok(())
}
#[instrument(level = "debug", name = "base_pre_modify", skip(_qs, _cand, me))]
fn pre_modify(
_qs: &QueryServerWriteTransaction,
_cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
@ -188,6 +187,7 @@ impl Plugin for Base {
Ok(())
}
#[instrument(level = "debug", name = "base_verify", skip(qs))]
fn verify(qs: &QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
// Search for class = *
let entries = match qs.internal_search(filter!(f_pres("class"))) {

View file

@ -25,6 +25,11 @@ impl Plugin for Domain {
"plugin_domain"
}
#[instrument(
level = "debug",
name = "domain_pre_create_transform",
skip(qs, cand, _ce)
)]
fn pre_create_transform(
qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
@ -76,6 +81,7 @@ impl Plugin for Domain {
})
}
#[instrument(level = "debug", name = "domain_pre_modify", skip(qs, cand, _me))]
fn pre_modify(
qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,

View file

@ -0,0 +1,925 @@
use crate::event::{CreateEvent, ModifyEvent};
use crate::filter::FilterInvalid;
use crate::prelude::*;
use kanidm_proto::v1::Filter as ProtoFilter;
use std::collections::BTreeMap;
use std::sync::Arc;
lazy_static! {
static ref CLASS_DYNGROUP: PartialValue = PartialValue::new_class("dyngroup");
}
#[derive(Clone)]
pub struct DynGroupCache {
insts: BTreeMap<Uuid, Filter<FilterInvalid>>,
}
impl Default for DynGroupCache {
fn default() -> Self {
DynGroupCache {
insts: BTreeMap::default(),
}
}
}
pub struct DynGroup;
impl DynGroup {
fn apply_dyngroup_change(
qs: &QueryServerWriteTransaction,
ident: &Identity,
pre_candidates: &mut Vec<Arc<EntrySealedCommitted>>,
candidates: &mut Vec<EntryInvalidCommitted>,
affected_uuids: &mut Vec<Uuid>,
expect: bool,
ident_internal: &Identity,
dyn_groups: &mut DynGroupCache,
n_dyn_groups: &[&Entry<EntrySealed, EntryCommitted>],
) -> Result<(), OperationError> {
if !ident.is_internal() {
// It should be impossible to trigger this right now due to protected plugin.
error!("It is currently an error to create a dynamic group");
return Err(OperationError::SystemProtectedObject);
}
// Search all the new groups first.
let filt = filter!(FC::Or(
n_dyn_groups
.into_iter()
.map(|e| f_eq("uuid", PartialValue::new_uuid(e.get_uuid())))
.collect()
));
let work_set = qs.internal_search_writeable(&filt)?;
// Go through them all and update the new groups.
for (pre, mut nd_group) in work_set.into_iter() {
let scope_f: ProtoFilter = nd_group
.get_ava_single_protofilter("dyngroup_filter")
.cloned()
.ok_or_else(|| {
admin_error!("Missing dyngroup_filter");
OperationError::InvalidEntryState
})?;
let scope_i = Filter::from_rw(ident_internal, &scope_f, qs).map_err(|e| {
admin_error!("dyngroup_filter validation failed {:?}", e);
e
})?;
let uuid = pre.get_uuid();
// Add our uuid as affected.
affected_uuids.push(uuid);
// Apply the filter and get all the uuids.
let entries = qs.internal_search(scope_i.clone()).map_err(|e| {
admin_error!("internal search failure -> {:?}", e);
e
})?;
let members = ValueSetRefer::from_iter(entries.iter().map(|e| e.get_uuid()));
if let Some(uuid_iter) = members.as_ref().and_then(|a| a.as_ref_uuid_iter()) {
affected_uuids.extend(uuid_iter);
}
if let Some(members) = members {
// Only set something if there is actually something to do!
nd_group.set_ava_set("member", members);
// push the entries to pre/cand
} else {
nd_group.purge_ava("member");
}
pre_candidates.push(pre);
candidates.push(nd_group);
// Insert to our new instances
if dyn_groups.insts.insert(uuid, scope_i).is_none() == expect {
admin_error!("dyngroup cache uuid conflict {}", uuid);
return Err(OperationError::InvalidState);
}
}
Ok(())
}
#[instrument(level = "debug", name = "dyngroup_reload", skip(qs))]
pub fn reload(qs: &QueryServerWriteTransaction) -> Result<(), OperationError> {
let ident_internal = Identity::from_internal();
// Internal search all our definitions.
let filt = filter!(f_eq("class", PartialValue::new_class("dyngroup")));
let entries = qs.internal_search(filt).map_err(|e| {
admin_error!("internal search failure -> {:?}", e);
e
})?;
let dyn_groups = qs.get_dyngroup_cache();
dyn_groups.insts.clear();
for nd_group in entries.into_iter() {
let scope_f: ProtoFilter = nd_group
.get_ava_single_protofilter("dyngroup_filter")
.cloned()
.ok_or_else(|| {
admin_error!("Missing dyngroup_filter");
OperationError::InvalidEntryState
})?;
let scope_i = Filter::from_rw(&ident_internal, &scope_f, qs).map_err(|e| {
admin_error!("dyngroup_filter validation failed {:?}", e);
e
})?;
let uuid = nd_group.get_uuid();
if dyn_groups.insts.insert(uuid, scope_i).is_some() {
admin_error!("dyngroup cache uuid conflict {}", uuid);
return Err(OperationError::InvalidState);
}
}
Ok(())
}
#[instrument(level = "debug", name = "dyngroup_post_create", skip(qs, cand, ce))]
pub fn post_create(
qs: &QueryServerWriteTransaction,
cand: &[Entry<EntrySealed, EntryCommitted>],
ce: &CreateEvent,
) -> Result<Vec<Uuid>, OperationError> {
let mut affected_uuids = Vec::with_capacity(cand.len());
let ident_internal = Identity::from_internal();
let (n_dyn_groups, entries): (Vec<&Entry<_, _>>, Vec<_>) = cand
.iter()
.partition(|entry| entry.attribute_equality("class", &CLASS_DYNGROUP));
let dyn_groups = qs.get_dyngroup_cache();
// For any other entries, check if they SHOULD trigger
// a dyn group inclusion. We do this FIRST because the new
// dyn groups will see the created entries on an internal search
// so we don't need to reference them.
//
let resolve_filter_cache = qs.get_resolve_filter_cache();
let mut pre_candidates = Vec::with_capacity(dyn_groups.insts.len() + cand.len());
let mut candidates = Vec::with_capacity(dyn_groups.insts.len() + cand.len());
trace!(?dyn_groups.insts);
for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
let dg_filter_valid = dg_filter
.validate(qs.get_schema())
.map_err(OperationError::SchemaViolation)
.and_then(|f| f.resolve(&ident_internal, None, Some(resolve_filter_cache)))?;
let matches: Vec<_> = entries
.iter()
.filter_map(|e| {
if e.entry_match_no_index(&dg_filter_valid) {
Some(e.get_uuid())
} else {
None
}
})
.collect();
if !matches.is_empty() {
let filt = filter!(f_eq("uuid", PartialValue::new_uuid(*dg_uuid)));
let mut work_set = qs.internal_search_writeable(&filt)?;
if let Some((pre, mut d_group)) = work_set.pop() {
matches
.iter()
.copied()
.for_each(|u| d_group.add_ava("member", Value::new_refer(u)));
affected_uuids.extend(matches.into_iter());
affected_uuids.push(*dg_uuid);
pre_candidates.push(pre);
candidates.push(d_group);
}
}
}
// If we created any dyn groups, populate them now.
// if the event is not internal, reject (for now)
if !n_dyn_groups.is_empty() {
trace!("considering new dyngroups");
Self::apply_dyngroup_change(
qs,
&ce.ident,
&mut pre_candidates,
&mut candidates,
&mut affected_uuids,
false,
&ident_internal,
dyn_groups,
n_dyn_groups.as_slice(),
)?;
}
// Write back the new changes.
debug_assert!(pre_candidates.len() == candidates.len());
// Write this stripe if populated.
if !pre_candidates.is_empty() {
qs.internal_batch_modify(pre_candidates, candidates)
.map_err(|e| {
admin_error!("Failed to commit dyngroup set {:?}", e);
e
})?;
}
Ok(affected_uuids)
}
#[instrument(
level = "debug",
name = "memberof_post_modify",
skip(qs, pre_cand, cand, me)
)]
pub fn post_modify(
qs: &QueryServerWriteTransaction,
pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
cand: &[Entry<EntrySealed, EntryCommitted>],
me: &ModifyEvent,
) -> Result<Vec<Uuid>, OperationError> {
let mut affected_uuids = Vec::with_capacity(cand.len());
let ident_internal = Identity::from_internal();
let resolve_filter_cache = qs.get_resolve_filter_cache();
// Probably should be filter here instead.
let (_, pre_entries): (Vec<&Arc<Entry<_, _>>>, Vec<_>) = pre_cand
.iter()
.partition(|entry| entry.attribute_equality("class", &CLASS_DYNGROUP));
let (n_dyn_groups, post_entries): (Vec<&Entry<_, _>>, Vec<_>) = cand
.iter()
.partition(|entry| entry.attribute_equality("class", &CLASS_DYNGROUP));
let dyn_groups = qs.get_dyngroup_cache();
let mut pre_candidates = Vec::with_capacity(dyn_groups.insts.len() + cand.len());
let mut candidates = Vec::with_capacity(dyn_groups.insts.len() + cand.len());
// If we modified a dyngroups member or filter, re-trigger it here.
// if the event is not internal, reject (for now)
// We do this *first* so that we don't accidentally include/exclude anything that
// changed in this op.
if !n_dyn_groups.is_empty() {
trace!("considering modified dyngroups");
Self::apply_dyngroup_change(
qs,
&me.ident,
&mut pre_candidates,
&mut candidates,
&mut affected_uuids,
true,
&ident_internal,
dyn_groups,
n_dyn_groups.as_slice(),
)?;
}
// If we modified anything else, check if a dyngroup is affected by it's change
// if it was a member.
trace!(?dyn_groups.insts);
for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
let dg_filter_valid = dg_filter
.validate(qs.get_schema())
.map_err(OperationError::SchemaViolation)
.and_then(|f| f.resolve(&ident_internal, None, Some(resolve_filter_cache)))?;
let matches: Vec<_> = pre_entries
.iter()
.zip(post_entries.iter())
.filter_map(|(pre, post)| {
let pre_t = pre.entry_match_no_index(&dg_filter_valid);
let post_t = post.entry_match_no_index(&dg_filter_valid);
if pre_t && !post_t {
Some(Err(post.get_uuid()))
} else if !pre_t && post_t {
Some(Ok(post.get_uuid()))
} else {
None
}
})
.collect();
if !matches.is_empty() {
let filt = filter!(f_eq("uuid", PartialValue::new_uuid(*dg_uuid)));
let mut work_set = qs.internal_search_writeable(&filt)?;
if let Some((pre, mut d_group)) = work_set.pop() {
matches.iter().copied().for_each(|choice| match choice {
Ok(u) => d_group.add_ava("member", Value::new_refer(u)),
Err(u) => d_group.remove_ava("member", &PartialValue::new_refer(u)),
});
affected_uuids.extend(matches.into_iter().map(|choice| match choice {
Ok(u) => u,
Err(u) => u,
}));
affected_uuids.push(*dg_uuid);
pre_candidates.push(pre);
candidates.push(d_group);
}
}
}
// Write back the new changes.
debug_assert!(pre_candidates.len() == candidates.len());
// Write this stripe if populated.
if !pre_candidates.is_empty() {
qs.internal_batch_modify(pre_candidates, candidates)
.map_err(|e| {
admin_error!("Failed to commit dyngroup set {:?}", e);
e
})?;
}
Ok(affected_uuids)
}
// No post_delete handler is needed as refint takes care of this for us.
pub fn verify(_qs: &QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
vec![]
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use kanidm_proto::v1::Filter as ProtoFilter;
const UUID_TEST_GROUP: Uuid = uuid::uuid!("7bfd9931-06c2-4608-8a46-78719bb746fe");
#[test]
fn test_create_dyngroup_add_new_group() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_group];
let create = vec![e_dyn];
run_create_test!(
Ok(()),
preload,
create,
None,
// Need to validate it did things
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
let members = d_group
.get_ava_set("member")
.expect("No members on dyn group");
assert!(members.to_refer_single() == Some(UUID_TEST_GROUP));
}
);
}
#[test]
fn test_create_dyngroup_add_matching_entry() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn];
let create = vec![e_group];
run_create_test!(
Ok(()),
preload,
create,
None,
// Need to validate it did things
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
let members = d_group
.get_ava_set("member")
.expect("No members on dyn group");
assert!(members.to_refer_single() == Some(UUID_TEST_GROUP));
}
);
}
#[test]
fn test_create_dyngroup_add_non_matching_entry() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq(
"name".to_string(),
"no_possible_match_to_be_found".to_string()
))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn];
let create = vec![e_group];
run_create_test!(
Ok(()),
preload,
create,
None,
// Need to validate it did things
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
assert!(d_group.get_ava_set("member").is_none());
}
);
}
#[test]
fn test_create_dyngroup_add_matching_entry_and_group() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![];
let create = vec![e_dyn, e_group];
run_create_test!(
Ok(()),
preload,
create,
None,
// Need to validate it did things
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
let members = d_group
.get_ava_set("member")
.expect("No members on dyn group");
assert!(members.to_refer_single() == Some(UUID_TEST_GROUP));
}
);
}
#[test]
fn test_modify_dyngroup_existing_dyngroup_filter_into_scope() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq(
"name".to_string(),
"no_such_entry_exists".to_string()
))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn, e_group];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("test_dyngroup"))),
ModifyList::new_list(vec![
Modify::Purged("dyngroup_filter".into()),
Modify::Present(
AttrString::from("dyngroup_filter"),
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
]),
None,
|_| {},
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
let members = d_group
.get_ava_set("member")
.expect("No members on dyn group");
assert!(members.to_refer_single() == Some(UUID_TEST_GROUP));
}
);
}
#[test]
fn test_modify_dyngroup_existing_dyngroup_filter_outof_scope() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn, e_group];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("test_dyngroup"))),
ModifyList::new_list(vec![
Modify::Purged("dyngroup_filter".into()),
Modify::Present(
AttrString::from("dyngroup_filter"),
Value::JsonFilt(ProtoFilter::Eq(
"name".to_string(),
"no_such_entry_exists".to_string()
))
)
]),
None,
|_| {},
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
assert!(d_group.get_ava_set("member").is_none());
}
);
}
#[test]
fn test_modify_dyngroup_existing_dyngroup_member_add() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn, e_group];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("test_dyngroup"))),
ModifyList::new_list(vec![Modify::Present(
AttrString::from("member"),
Value::new_refer(*UUID_ADMIN)
)]),
None,
|_| {},
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
let members = d_group
.get_ava_set("member")
.expect("No members on dyn group");
// We assert to refer single here because we should have "removed" uuid_admin being added
// at all.
assert!(members.to_refer_single() == Some(UUID_TEST_GROUP));
}
);
}
#[test]
fn test_modify_dyngroup_existing_dyngroup_member_remove() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn, e_group];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("test_dyngroup"))),
ModifyList::new_list(vec![Modify::Purged(AttrString::from("member"),)]),
None,
|_| {},
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
let members = d_group
.get_ava_set("member")
.expect("No members on dyn group");
// We assert to refer single here because we should have re-added the members
assert!(members.to_refer_single() == Some(UUID_TEST_GROUP));
}
);
}
#[test]
fn test_modify_dyngroup_into_matching_entry() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("not_testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn, e_group];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("not_testgroup"))),
ModifyList::new_list(vec![
Modify::Purged(AttrString::from("name"),),
Modify::Present(AttrString::from("name"), Value::new_iname("testgroup"))
]),
None,
|_| {},
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
let members = d_group
.get_ava_set("member")
.expect("No members on dyn group");
assert!(members.to_refer_single() == Some(UUID_TEST_GROUP));
}
);
}
#[test]
fn test_modify_dyngroup_into_non_matching_entry() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn, e_group];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("testgroup"))),
ModifyList::new_list(vec![
Modify::Purged(AttrString::from("name"),),
Modify::Present(AttrString::from("name"), Value::new_iname("not_testgroup"))
]),
None,
|_| {},
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
assert!(d_group.get_ava_set("member").is_none());
}
);
}
#[test]
fn test_delete_dyngroup_matching_entry() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn, e_group];
run_delete_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("testgroup"))),
None,
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"name",
PartialValue::new_iname("test_dyngroup")
)))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
assert!(d_group.get_ava_set("member").is_none());
}
);
}
#[test]
fn test_delete_dyngroup_group() {
let e_dyn = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("group")),
("class", Value::new_class("dyngroup")),
("name", Value::new_iname("test_dyngroup")),
(
"dyngroup_filter",
Value::JsonFilt(ProtoFilter::Eq("name".to_string(), "testgroup".to_string()))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
("uuid", Value::new_uuid(UUID_TEST_GROUP))
);
let preload = vec![e_dyn, e_group];
run_delete_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("test_dyngroup"))),
None,
|qs: &QueryServerWriteTransaction| {
// Note we check memberof is empty here!
let cands = qs
.internal_search(filter!(f_eq("name", PartialValue::new_iname("testgroup"))))
.expect("Internal search failure");
let d_group = cands.get(0).expect("Unable to access group.");
assert!(d_group.get_ava_set("memberof").is_none());
}
);
}
}

View file

@ -10,7 +10,7 @@
// As a result, we first need to run refint to clean up all dangling references, then memberof
// fixes the graph of memberships
use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntrySealed, EntryTuple};
use crate::entry::{Entry, EntryCommitted, EntrySealed, EntryTuple};
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent};
use crate::plugins::Plugin;
use crate::prelude::*;
@ -20,7 +20,6 @@ use std::collections::BTreeSet;
use hashbrown::HashMap;
use std::sync::Arc;
use uuid::Uuid;
lazy_static! {
static ref CLASS_GROUP: PartialValue = PartialValue::new_class("group");
@ -215,14 +214,18 @@ impl Plugin for MemberOf {
"memberof"
}
#[instrument(level = "debug", name = "memberof_post_create", skip(qs, cand, ce))]
fn post_create(
qs: &QueryServerWriteTransaction,
cand: &[Entry<EntrySealed, EntryCommitted>],
_ce: &CreateEvent,
ce: &CreateEvent,
) -> Result<(), OperationError> {
let dyngroup_change = super::dyngroup::DynGroup::post_create(qs, cand, ce)?;
let group_affect = cand
.iter()
.map(|e| e.get_uuid())
.chain(dyngroup_change.into_iter())
.chain(
cand.iter()
.filter_map(|e| {
@ -240,16 +243,24 @@ impl Plugin for MemberOf {
apply_memberof(qs, group_affect)
}
#[instrument(
level = "debug",
name = "memberof_post_modify",
skip(qs, pre_cand, cand, me)
)]
fn post_modify(
qs: &QueryServerWriteTransaction,
pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
cand: &[Entry<EntrySealed, EntryCommitted>],
_me: &ModifyEvent,
me: &ModifyEvent,
) -> Result<(), OperationError> {
let dyngroup_change = super::dyngroup::DynGroup::post_modify(qs, pre_cand, cand, me)?;
// TODO: Limit this to when it's a class, member, mo, dmo change instead.
let group_affect = cand
.iter()
.map(|post| post.get_uuid())
.chain(dyngroup_change.into_iter())
.chain(
pre_cand
.iter()
@ -278,6 +289,7 @@ impl Plugin for MemberOf {
apply_memberof(qs, group_affect)
}
/*
fn pre_delete(
_qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
@ -298,11 +310,13 @@ impl Plugin for MemberOf {
cand.iter_mut().for_each(|e| e.apply_modlist(&mo_purge));
Ok(())
}
*/
#[instrument(level = "debug", name = "memberof_post_delete", skip(qs, cand, _de))]
fn post_delete(
qs: &QueryServerWriteTransaction,
cand: &[Entry<EntrySealed, EntryCommitted>],
_ce: &DeleteEvent,
_de: &DeleteEvent,
) -> Result<(), OperationError> {
// Similar condition to create - we only trigger updates on groups's members,
// so that they can find they are no longer a mo of what was deleted.
@ -322,6 +336,7 @@ impl Plugin for MemberOf {
apply_memberof(qs, group_affect)
}
#[instrument(level = "debug", name = "memberof_verify", skip(qs))]
fn verify(qs: &QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
let mut r = Vec::new();

View file

@ -13,6 +13,7 @@ use tracing::trace_span;
mod attrunique;
mod base;
mod domain;
pub(crate) mod dyngroup;
mod failure;
mod gidnumber;
mod memberof;
@ -181,8 +182,8 @@ impl Plugins {
) -> Result<(), OperationError> {
spanned!("plugins::run_post_modify", {
refint::ReferentialIntegrity::post_modify(qs, pre_cand, cand, me)
.and_then(|_| memberof::MemberOf::post_modify(qs, pre_cand, cand, me))
.and_then(|_| spn::Spn::post_modify(qs, pre_cand, cand, me))
.and_then(|_| memberof::MemberOf::post_modify(qs, pre_cand, cand, me))
})
}
@ -216,6 +217,7 @@ impl Plugins {
run_verify_plugin!(qs, results, base::Base);
run_verify_plugin!(qs, results, attrunique::AttrUnique);
run_verify_plugin!(qs, results, refint::ReferentialIntegrity);
run_verify_plugin!(qs, results, dyngroup::DynGroup);
run_verify_plugin!(qs, results, memberof::MemberOf);
run_verify_plugin!(qs, results, spn::Spn);
})

View file

@ -61,6 +61,11 @@ impl Plugin for Oauth2Secrets {
"plugin_oauth2_secrets"
}
#[instrument(
level = "debug",
name = "oauth2_pre_create_transform",
skip(_qs, cand, _ce)
)]
fn pre_create_transform(
_qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
@ -69,6 +74,7 @@ impl Plugin for Oauth2Secrets {
cand.iter_mut().try_for_each(|e| oauth2_transform!(e))
}
#[instrument(level = "debug", name = "oauth2_pre_modify", skip(_qs, cand, _me))]
fn pre_modify(
_qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,

View file

@ -14,6 +14,11 @@ impl Plugin for PasswordImport {
"plugin_password_import"
}
#[instrument(
level = "debug",
name = "password_import_pre_create_transform",
skip(_qs, cand, _ce)
)]
fn pre_create_transform(
_qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
@ -57,6 +62,11 @@ impl Plugin for PasswordImport {
})
}
#[instrument(
level = "debug",
name = "password_import_pre_modify",
skip(_qs, cand, _me)
)]
fn pre_modify(
_qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,

View file

@ -36,12 +36,14 @@ lazy_static! {
static ref PVCLASS_DOMAIN_INFO: PartialValue = PartialValue::new_class("domain_info");
static ref PVCLASS_SYSTEM_INFO: PartialValue = PartialValue::new_class("system_info");
static ref PVCLASS_SYSTEM_CONFIG: PartialValue = PartialValue::new_class("system_config");
static ref PVCLASS_DYNGROUP: PartialValue = PartialValue::new_class("dyngroup");
static ref VCLASS_SYSTEM: Value = Value::new_class("system");
static ref VCLASS_TOMBSTONE: Value = Value::new_class("tombstone");
static ref VCLASS_RECYCLED: Value = Value::new_class("recycled");
static ref VCLASS_DOMAIN_INFO: Value = Value::new_class("domain_info");
static ref VCLASS_SYSTEM_INFO: Value = Value::new_class("system_info");
static ref VCLASS_SYSTEM_CONFIG: Value = Value::new_class("system_config");
static ref VCLASS_DYNGROUP: Value = Value::new_class("dyngroup");
}
impl Plugin for Protected {
@ -49,6 +51,7 @@ impl Plugin for Protected {
"plugin_protected"
}
#[instrument(level = "debug", name = "protected_pre_create", skip(_qs, cand, ce))]
fn pre_create(
_qs: &QueryServerWriteTransaction,
// List of what we will commit that is valid?
@ -67,6 +70,7 @@ impl Plugin for Protected {
|| cand.attribute_equality("class", &PVCLASS_SYSTEM_CONFIG)
|| cand.attribute_equality("class", &PVCLASS_TOMBSTONE)
|| cand.attribute_equality("class", &PVCLASS_RECYCLED)
|| cand.attribute_equality("class", &PVCLASS_DYNGROUP)
{
Err(OperationError::SystemProtectedObject)
} else {
@ -75,6 +79,7 @@ impl Plugin for Protected {
})
}
#[instrument(level = "debug", name = "protected_pre_modify", skip(_qs, cand, me))]
fn pre_modify(
_qs: &QueryServerWriteTransaction,
// Should these be EntrySealed?
@ -94,6 +99,7 @@ impl Plugin for Protected {
|| v == &(*VCLASS_DOMAIN_INFO)
|| v == &(*VCLASS_SYSTEM_INFO)
|| v == &(*VCLASS_SYSTEM_CONFIG)
|| v == &(*VCLASS_DYNGROUP)
|| v == &(*VCLASS_TOMBSTONE)
|| v == &(*VCLASS_RECYCLED))
{
@ -110,6 +116,7 @@ impl Plugin for Protected {
cand.iter().try_fold((), |(), cand| {
if cand.attribute_equality("class", &PVCLASS_TOMBSTONE)
|| cand.attribute_equality("class", &PVCLASS_RECYCLED)
|| cand.attribute_equality("class", &PVCLASS_DYNGROUP)
{
Err(OperationError::SystemProtectedObject)
} else {
@ -143,6 +150,7 @@ impl Plugin for Protected {
})
}
#[instrument(level = "debug", name = "protected_pre_delete", skip(_qs, cand, de))]
fn pre_delete(
_qs: &QueryServerWriteTransaction,
// Should these be EntrySealed
@ -161,6 +169,7 @@ impl Plugin for Protected {
|| cand.attribute_equality("class", &PVCLASS_SYSTEM_CONFIG)
|| cand.attribute_equality("class", &PVCLASS_TOMBSTONE)
|| cand.attribute_equality("class", &PVCLASS_RECYCLED)
|| cand.attribute_equality("class", &PVCLASS_DYNGROUP)
{
Err(OperationError::SystemProtectedObject)
} else {

View file

@ -82,6 +82,7 @@ impl Plugin for ReferentialIntegrity {
// so we can assert stronger trust in it's correct operation and interaction
// in complex scenarioes - It actually simplifies the check from "could
// be in cand AND db" to simply "is it in the DB?".
#[instrument(level = "debug", name = "refint_post_create", skip(qs, cand, _ce))]
fn post_create(
qs: &QueryServerWriteTransaction,
cand: &[Entry<EntrySealed, EntryCommitted>],
@ -118,6 +119,11 @@ impl Plugin for ReferentialIntegrity {
Self::check_uuids_exist(qs, i)
}
#[instrument(
level = "debug",
name = "refint_post_modify",
skip(qs, _pre_cand, _cand, me)
)]
fn post_modify(
qs: &QueryServerWriteTransaction,
_pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
@ -157,6 +163,7 @@ impl Plugin for ReferentialIntegrity {
Self::check_uuids_exist(qs, i)
}
#[instrument(level = "debug", name = "refint_post_delete", skip(qs, cand, _ce))]
fn post_delete(
qs: &QueryServerWriteTransaction,
cand: &[Entry<EntrySealed, EntryCommitted>],
@ -209,6 +216,7 @@ impl Plugin for ReferentialIntegrity {
qs.internal_batch_modify(pre_candidates, candidates)
}
#[instrument(level = "debug", name = "verify", skip(qs))]
fn verify(qs: &QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
// Get all entries as cand
// build a cand-uuid set

View file

@ -26,6 +26,11 @@ impl Plugin for Spn {
}
// hook on pre-create and modify to generate / validate.
#[instrument(
level = "debug",
name = "spn_pre_create_transform",
skip(qs, cand, _ce)
)]
fn pre_create_transform(
qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
@ -60,6 +65,7 @@ impl Plugin for Spn {
Ok(())
}
#[instrument(level = "debug", name = "spn_pre_modify", skip(qs, cand, _me))]
fn pre_modify(
qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
@ -90,6 +96,11 @@ impl Plugin for Spn {
Ok(())
}
#[instrument(
level = "debug",
name = "spn_post_modify",
skip(qs, pre_cand, cand, _ce)
)]
fn post_modify(
qs: &QueryServerWriteTransaction,
// List of what we modified that was valid?
@ -133,6 +144,7 @@ impl Plugin for Spn {
)
}
#[instrument(level = "debug", name = "spn_verify", skip(qs))]
fn verify(qs: &QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
// Verify that all items with spn's have valid spns.
// We need to consider the case that an item has a different origin domain too,

View file

@ -27,6 +27,7 @@ use crate::event::{
use crate::filter::{Filter, FilterInvalid, FilterValid, FilterValidResolved};
use crate::identity::IdentityId;
use crate::modify::{Modify, ModifyInvalid, ModifyList, ModifyValid};
use crate::plugins::dyngroup::{DynGroup, DynGroupCache};
use crate::plugins::Plugins;
use crate::repl::cid::Cid;
use crate::schema::{
@ -56,6 +57,13 @@ lazy_static! {
static ref PVACP_ENABLE_FALSE: PartialValue = PartialValue::new_bool(false);
}
#[derive(Debug, Clone, PartialOrd, PartialEq, Eq)]
enum ServerPhase {
Bootstrap,
SchemaReady,
Running,
}
#[derive(Debug, Clone)]
struct DomainInfo {
d_uuid: Uuid,
@ -65,6 +73,7 @@ struct DomainInfo {
#[derive(Clone)]
pub struct QueryServer {
phase: Arc<CowCell<ServerPhase>>,
s_uuid: Uuid,
d_info: Arc<CowCell<DomainInfo>>,
be: Backend,
@ -74,6 +83,7 @@ pub struct QueryServer {
write_ticket: Arc<Semaphore>,
resolve_filter_cache:
Arc<ARCache<(IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>>,
dyngroup_cache: Arc<CowCell<DynGroupCache>>,
}
pub struct QueryServerReadTransaction<'a> {
@ -94,6 +104,7 @@ unsafe impl<'a> Send for QueryServerReadTransaction<'a> {}
pub struct QueryServerWriteTransaction<'a> {
committed: bool,
phase: CowCellWriteTxn<'a, ServerPhase>,
d_info: CowCellWriteTxn<'a, DomainInfo>,
cid: Cid,
be_txn: BackendWriteTransaction<'a>,
@ -112,6 +123,7 @@ pub struct QueryServerWriteTransaction<'a> {
_write_ticket: SemaphorePermit<'a>,
resolve_filter_cache:
Cell<ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>>,
dyngroup_cache: Cell<CowCellWriteTxn<'a, DynGroupCache>>,
}
pub(crate) struct ModifyPartial<'a> {
@ -982,10 +994,15 @@ impl QueryServer {
d_display: domain_name,
}));
let dyngroup_cache = Arc::new(CowCell::new(DynGroupCache::default()));
let phase = Arc::new(CowCell::new(ServerPhase::Bootstrap));
// log_event!(log, "Starting query worker ...");
#[allow(clippy::expect_used)]
QueryServer {
phase,
s_uuid,
d_info,
be,
@ -1000,6 +1017,7 @@ impl QueryServer {
.build()
.expect("Failed to build resolve_filter_cache"),
),
dyngroup_cache,
}
}
@ -1059,6 +1077,7 @@ impl QueryServer {
let schema_write = self.schema.write();
let be_txn = self.be.write();
let d_info = self.d_info.write();
let phase = self.phase.write();
#[allow(clippy::expect_used)]
let ts_max = be_txn.get_db_ts_max(ts).expect("Unable to get db_ts_max");
@ -1072,6 +1091,7 @@ impl QueryServer {
// The commited flag is however used for abort-specific code in drop
// which today I don't think we have ... yet.
committed: false,
phase,
d_info,
cid,
be_txn,
@ -1085,6 +1105,7 @@ impl QueryServer {
_db_ticket: db_ticket,
_write_ticket: write_ticket,
resolve_filter_cache: Cell::new(self.resolve_filter_cache.read()),
dyngroup_cache: Cell::new(self.dyngroup_cache.write()),
}
}
@ -1132,7 +1153,11 @@ impl QueryServer {
// Force the schema to reload - this is so that any changes to index slope
// analysis are now reflected correctly.
let slope_reload = task::block_on(self.write_async(ts));
//
// A side effect of these reloads is that other plugins or elements that reload
// on schema change are now setup.
let mut slope_reload = task::block_on(self.write_async(ts));
slope_reload.set_phase(ServerPhase::SchemaReady);
slope_reload.force_schema_reload();
slope_reload.commit()?;
@ -1178,10 +1203,11 @@ impl QueryServer {
migrate_txn.commit()?;
// Migrations complete. Init idm will now set the version as needed.
let ts_write_3 = task::block_on(self.write_async(ts));
ts_write_3
.initialise_idm()
.and_then(|_| ts_write_3.commit())?;
let mut ts_write_3 = task::block_on(self.write_async(ts));
ts_write_3.initialise_idm().and_then(|_| {
ts_write_3.set_phase(ServerPhase::Running);
ts_write_3.commit()
})?;
// TODO: work out if we've actually done any migrations before printing this
admin_debug!("Database version check and migrations success! ☀️ ");
Ok(())
@ -2165,6 +2191,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
})
}
pub(crate) fn get_dyngroup_cache(&self) -> &mut DynGroupCache {
unsafe {
let mptr = self.dyngroup_cache.as_ptr();
(*mptr).get_mut()
}
}
/// Migrate 2 to 3 changes the name, domain_name types from iutf8 to iname.
pub fn migrate_2_to_3(&self) -> Result<(), OperationError> {
spanned!("server::migrate_2_to_3", {
@ -2599,9 +2632,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP,
JSON_SCHEMA_ATTR_PASSKEYS,
JSON_SCHEMA_ATTR_DEVICEKEYS,
JSON_SCHEMA_ATTR_DYNGROUP_FILTER,
JSON_SCHEMA_CLASS_PERSON,
JSON_SCHEMA_CLASS_ORGPERSON,
JSON_SCHEMA_CLASS_GROUP,
JSON_SCHEMA_CLASS_DYNGROUP,
JSON_SCHEMA_CLASS_ACCOUNT,
JSON_SCHEMA_CLASS_SERVICE_ACCOUNT,
JSON_SCHEMA_CLASS_DOMAIN_INFO,
@ -2669,6 +2704,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
// Create any system default access profile entries.
let idm_entries = [
// Builtin dyn groups,
JSON_IDM_ALL_PERSONS,
// Builtin groups
JSON_IDM_PEOPLE_MANAGE_PRIV_V1,
JSON_IDM_PEOPLE_ACCOUNT_PASSWORD_IMPORT_PRIV_V1,
@ -2763,66 +2800,72 @@ impl<'a> QueryServerWriteTransaction<'a> {
Ok(())
}
#[instrument(level = "info", name = "reload_schema", skip(self))]
fn reload_schema(&mut self) -> Result<(), OperationError> {
spanned!("server::reload_schema", {
// supply entries to the writable schema to reload from.
// find all attributes.
let filt = filter!(f_eq("class", PVCLASS_ATTRIBUTETYPE.clone()));
let res = self.internal_search(filt).map_err(|e| {
admin_error!("reload schema internal search failed {:?}", e);
e
})?;
// load them.
let attributetypes: Result<Vec<_>, _> =
res.iter().map(|e| SchemaAttribute::try_from(e)).collect();
let attributetypes = attributetypes.map_err(|e| {
admin_error!("reload schema attributetypes {:?}", e);
e
})?;
// supply entries to the writable schema to reload from.
// find all attributes.
let filt = filter!(f_eq("class", PVCLASS_ATTRIBUTETYPE.clone()));
let res = self.internal_search(filt).map_err(|e| {
admin_error!("reload schema internal search failed {:?}", e);
e
})?;
// load them.
let attributetypes: Result<Vec<_>, _> =
res.iter().map(|e| SchemaAttribute::try_from(e)).collect();
let attributetypes = attributetypes.map_err(|e| {
admin_error!("reload schema attributetypes {:?}", e);
e
})?;
self.schema.update_attributes(attributetypes).map_err(|e| {
admin_error!("reload schema update attributetypes {:?}", e);
e
})?;
self.schema.update_attributes(attributetypes).map_err(|e| {
admin_error!("reload schema update attributetypes {:?}", e);
e
})?;
// find all classes
let filt = filter!(f_eq("class", PVCLASS_CLASSTYPE.clone()));
let res = self.internal_search(filt).map_err(|e| {
admin_error!("reload schema internal search failed {:?}", e);
e
})?;
// load them.
let classtypes: Result<Vec<_>, _> =
res.iter().map(|e| SchemaClass::try_from(e)).collect();
let classtypes = classtypes.map_err(|e| {
admin_error!("reload schema classtypes {:?}", e);
e
})?;
// find all classes
let filt = filter!(f_eq("class", PVCLASS_CLASSTYPE.clone()));
let res = self.internal_search(filt).map_err(|e| {
admin_error!("reload schema internal search failed {:?}", e);
e
})?;
// load them.
let classtypes: Result<Vec<_>, _> = res.iter().map(|e| SchemaClass::try_from(e)).collect();
let classtypes = classtypes.map_err(|e| {
admin_error!("reload schema classtypes {:?}", e);
e
})?;
self.schema.update_classes(classtypes).map_err(|e| {
admin_error!("reload schema update classtypes {:?}", e);
e
})?;
self.schema.update_classes(classtypes).map_err(|e| {
admin_error!("reload schema update classtypes {:?}", e);
e
})?;
// validate.
let valid_r = self.schema.validate();
// validate.
let valid_r = self.schema.validate();
// Translate the result.
if valid_r.is_empty() {
// Now use this to reload the backend idxmeta
trace!("Reloading idxmeta ...");
self.be_txn
.update_idxmeta(self.schema.reload_idxmeta())
.map_err(|e| {
admin_error!("reload schema update idxmeta {:?}", e);
e
})
} else {
// Log the failures?
admin_error!("Schema reload failed -> {:?}", valid_r);
Err(OperationError::ConsistencyError(valid_r))
}
})
// Translate the result.
if valid_r.is_empty() {
// Now use this to reload the backend idxmeta
trace!("Reloading idxmeta ...");
self.be_txn
.update_idxmeta(self.schema.reload_idxmeta())
.map_err(|e| {
admin_error!("reload schema update idxmeta {:?}", e);
e
})
} else {
// Log the failures?
admin_error!("Schema reload failed -> {:?}", valid_r);
Err(OperationError::ConsistencyError(valid_r))
}?;
// Trigger reloads on services that require post-schema reloads.
// Mainly this is plugins.
if *self.phase >= ServerPhase::SchemaReady {
DynGroup::reload(self)?;
}
Ok(())
}
fn reload_accesscontrols(&mut self) -> Result<(), OperationError> {
@ -3053,6 +3096,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.changed_domain.get()
}
fn set_phase(&mut self, phase: ServerPhase) {
*self.phase = phase
}
pub fn commit(mut self) -> Result<(), OperationError> {
// This could be faster if we cache the set of classes changed
// in an operation so we can check if we need to do the reload or not
@ -3081,11 +3128,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
// Now destructure the transaction ready to reset it.
let QueryServerWriteTransaction {
committed,
phase,
be_txn,
schema,
d_info,
accesscontrols,
cid,
dyngroup_cache,
..
} = self;
debug_assert!(!committed);
@ -3102,6 +3151,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
schema
.commit()
.map(|_| d_info.commit())
.map(|_| phase.commit())
.map(|_| dyngroup_cache.into_inner().commit())
.and_then(|_| accesscontrols.commit())
.and_then(|_| be_txn.commit())
} else {
@ -3135,7 +3186,7 @@ mod tests {
let se1 = unsafe { SearchEvent::new_impersonate_entry(admin.clone(), filt.clone()) };
let se2 = unsafe { SearchEvent::new_impersonate_entry(admin, filt) };
let e = entry_init!(
let mut e = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("person")),
("name", Value::new_iname("testperson")),
@ -3159,6 +3210,11 @@ mod tests {
debug!("--> {:?}", r2);
assert!(r2.len() == 1);
// We apply some member-of in the server now, so we add these before we seal.
e.add_ava("class", Value::new_class("memberof"));
e.add_ava("memberof", Value::new_refer(UUID_IDM_ALL_PERSONS));
e.add_ava("directmemberof", Value::new_refer(UUID_IDM_ALL_PERSONS));
let expected = unsafe { vec![Arc::new(e.into_sealed_committed())] };
assert_eq!(r2, expected);