20250314 remove protected plugin ()

Removes the protected plugin into an access control module so that it's outputs can be properly represented in effective access checks.
This commit is contained in:
Firstyear 2025-04-01 11:00:56 +10:00 committed by GitHub
parent ec3db91da0
commit a2eae53328
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 933 additions and 1007 deletions

49
Cargo.lock generated
View file

@ -232,9 +232,9 @@ dependencies = [
[[package]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.4.21" version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cf008e5e1a9e9e22a7d3c9a4992e21a350290069e36d8fb72304ed17e8f2d2" checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
dependencies = [ dependencies = [
"flate2", "flate2",
"futures-core", "futures-core",
@ -1167,9 +1167,9 @@ dependencies = [
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.4.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
"serde", "serde",
@ -2532,14 +2532,15 @@ dependencies = [
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.61" version = "0.1.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
"iana-time-zone-haiku", "iana-time-zone-haiku",
"js-sys", "js-sys",
"log",
"wasm-bindgen", "wasm-bindgen",
"windows-core", "windows-core",
] ]
@ -2594,9 +2595,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_locid_transform_data" name = "icu_locid_transform_data"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
[[package]] [[package]]
name = "icu_normalizer" name = "icu_normalizer"
@ -2618,9 +2619,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_normalizer_data" name = "icu_normalizer_data"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
[[package]] [[package]]
name = "icu_properties" name = "icu_properties"
@ -2639,9 +2640,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_properties_data" name = "icu_properties_data"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
[[package]] [[package]]
name = "icu_provider" name = "icu_provider"
@ -3483,9 +3484,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.26" version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]] [[package]]
name = "lru" name = "lru"
@ -3948,9 +3949,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.1" version = "1.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
[[package]] [[package]]
name = "openssl" name = "openssl"
@ -4483,9 +4484,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-udp" name = "quinn-udp"
version = "0.5.10" version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5"
dependencies = [ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
@ -4947,9 +4948,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.0" version = "0.103.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f" checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -5576,9 +5577,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.40" version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
@ -5599,9 +5600,9 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.21" version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",

View file

@ -22,6 +22,8 @@ pub enum Attribute {
AcpCreateClass, AcpCreateClass,
AcpEnable, AcpEnable,
AcpModifyClass, AcpModifyClass,
AcpModifyPresentClass,
AcpModifyRemoveClass,
AcpModifyPresentAttr, AcpModifyPresentAttr,
AcpModifyRemovedAttr, AcpModifyRemovedAttr,
AcpReceiver, AcpReceiver,
@ -255,6 +257,8 @@ impl Attribute {
Attribute::AcpCreateClass => ATTR_ACP_CREATE_CLASS, Attribute::AcpCreateClass => ATTR_ACP_CREATE_CLASS,
Attribute::AcpEnable => ATTR_ACP_ENABLE, Attribute::AcpEnable => ATTR_ACP_ENABLE,
Attribute::AcpModifyClass => ATTR_ACP_MODIFY_CLASS, Attribute::AcpModifyClass => ATTR_ACP_MODIFY_CLASS,
Attribute::AcpModifyPresentClass => ATTR_ACP_MODIFY_PRESENT_CLASS,
Attribute::AcpModifyRemoveClass => ATTR_ACP_MODIFY_REMOVE_CLASS,
Attribute::AcpModifyPresentAttr => ATTR_ACP_MODIFY_PRESENTATTR, Attribute::AcpModifyPresentAttr => ATTR_ACP_MODIFY_PRESENTATTR,
Attribute::AcpModifyRemovedAttr => ATTR_ACP_MODIFY_REMOVEDATTR, Attribute::AcpModifyRemovedAttr => ATTR_ACP_MODIFY_REMOVEDATTR,
Attribute::AcpReceiver => ATTR_ACP_RECEIVER, Attribute::AcpReceiver => ATTR_ACP_RECEIVER,
@ -440,6 +444,8 @@ impl Attribute {
ATTR_ACP_CREATE_CLASS => Attribute::AcpCreateClass, ATTR_ACP_CREATE_CLASS => Attribute::AcpCreateClass,
ATTR_ACP_ENABLE => Attribute::AcpEnable, ATTR_ACP_ENABLE => Attribute::AcpEnable,
ATTR_ACP_MODIFY_CLASS => Attribute::AcpModifyClass, ATTR_ACP_MODIFY_CLASS => Attribute::AcpModifyClass,
ATTR_ACP_MODIFY_PRESENT_CLASS => Attribute::AcpModifyPresentClass,
ATTR_ACP_MODIFY_REMOVE_CLASS => Attribute::AcpModifyRemoveClass,
ATTR_ACP_MODIFY_PRESENTATTR => Attribute::AcpModifyPresentAttr, ATTR_ACP_MODIFY_PRESENTATTR => Attribute::AcpModifyPresentAttr,
ATTR_ACP_MODIFY_REMOVEDATTR => Attribute::AcpModifyRemovedAttr, ATTR_ACP_MODIFY_REMOVEDATTR => Attribute::AcpModifyRemovedAttr,
ATTR_ACP_RECEIVER => Attribute::AcpReceiver, ATTR_ACP_RECEIVER => Attribute::AcpReceiver,

View file

@ -62,6 +62,8 @@ pub const ATTR_ACP_CREATE_ATTR: &str = "acp_create_attr";
pub const ATTR_ACP_CREATE_CLASS: &str = "acp_create_class"; pub const ATTR_ACP_CREATE_CLASS: &str = "acp_create_class";
pub const ATTR_ACP_ENABLE: &str = "acp_enable"; pub const ATTR_ACP_ENABLE: &str = "acp_enable";
pub const ATTR_ACP_MODIFY_CLASS: &str = "acp_modify_class"; pub const ATTR_ACP_MODIFY_CLASS: &str = "acp_modify_class";
pub const ATTR_ACP_MODIFY_PRESENT_CLASS: &str = "acp_modify_present_class";
pub const ATTR_ACP_MODIFY_REMOVE_CLASS: &str = "acp_modify_remove_class";
pub const ATTR_ACP_MODIFY_PRESENTATTR: &str = "acp_modify_presentattr"; pub const ATTR_ACP_MODIFY_PRESENTATTR: &str = "acp_modify_presentattr";
pub const ATTR_ACP_MODIFY_REMOVEDATTR: &str = "acp_modify_removedattr"; pub const ATTR_ACP_MODIFY_REMOVEDATTR: &str = "acp_modify_removedattr";
pub const ATTR_ACP_RECEIVER_GROUP: &str = "acp_receiver_group"; pub const ATTR_ACP_RECEIVER_GROUP: &str = "acp_receiver_group";

View file

@ -33,7 +33,7 @@ pub enum ScimAttributeEffectiveAccess {
/// All attributes on the entry have this permission granted /// All attributes on the entry have this permission granted
Grant, Grant,
/// All attributes on the entry have this permission denied /// All attributes on the entry have this permission denied
Denied, Deny,
/// The following attributes on the entry have this permission granted /// The following attributes on the entry have this permission granted
Allow(BTreeSet<Attribute>), Allow(BTreeSet<Attribute>),
} }
@ -43,7 +43,7 @@ impl ScimAttributeEffectiveAccess {
pub fn check(&self, attr: &Attribute) -> bool { pub fn check(&self, attr: &Attribute) -> bool {
match self { match self {
Self::Grant => true, Self::Grant => true,
Self::Denied => false, Self::Deny => false,
Self::Allow(set) => set.contains(attr), Self::Allow(set) => set.contains(attr),
} }
} }

View file

@ -330,6 +330,10 @@ pub const UUID_SCHEMA_ATTR_DOMAIN_ALLOW_EASTER_EGGS: Uuid =
pub const UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: Uuid = pub const UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000187"); uuid!("00000000-0000-0000-0000-ffff00000187");
pub const UUID_SCHEMA_ATTR_INDEXED: Uuid = uuid!("00000000-0000-0000-0000-ffff00000188"); pub const UUID_SCHEMA_ATTR_INDEXED: Uuid = uuid!("00000000-0000-0000-0000-ffff00000188");
pub const UUID_SCHEMA_ATTR_ACP_MODIFY_PRESENT_CLASS: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000189");
pub const UUID_SCHEMA_ATTR_ACP_MODIFY_REMOVE_CLASS: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000190");
// System and domain infos // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -599,19 +599,19 @@ impl IdmServerProxyWriteTransaction<'_> {
} }
let eperm_search_primary_cred = match &eperm.search { let eperm_search_primary_cred = match &eperm.search {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential), Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
}; };
let eperm_mod_primary_cred = match &eperm.modify_pres { let eperm_mod_primary_cred = match &eperm.modify_pres {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential), Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
}; };
let eperm_rem_primary_cred = match &eperm.modify_rem { let eperm_rem_primary_cred = match &eperm.modify_rem {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential), Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
}; };
@ -620,19 +620,19 @@ impl IdmServerProxyWriteTransaction<'_> {
eperm_search_primary_cred && eperm_mod_primary_cred && eperm_rem_primary_cred; eperm_search_primary_cred && eperm_mod_primary_cred && eperm_rem_primary_cred;
let eperm_search_passkeys = match &eperm.search { let eperm_search_passkeys = match &eperm.search {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys), Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
}; };
let eperm_mod_passkeys = match &eperm.modify_pres { let eperm_mod_passkeys = match &eperm.modify_pres {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys), Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
}; };
let eperm_rem_passkeys = match &eperm.modify_rem { let eperm_rem_passkeys = match &eperm.modify_rem {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys), Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
}; };
@ -640,19 +640,19 @@ impl IdmServerProxyWriteTransaction<'_> {
let passkeys_can_edit = eperm_search_passkeys && eperm_mod_passkeys && eperm_rem_passkeys; let passkeys_can_edit = eperm_search_passkeys && eperm_mod_passkeys && eperm_rem_passkeys;
let eperm_search_attested_passkeys = match &eperm.search { let eperm_search_attested_passkeys = match &eperm.search {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys), Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
}; };
let eperm_mod_attested_passkeys = match &eperm.modify_pres { let eperm_mod_attested_passkeys = match &eperm.modify_pres {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys), Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
}; };
let eperm_rem_attested_passkeys = match &eperm.modify_rem { let eperm_rem_attested_passkeys = match &eperm.modify_rem {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys), Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
}; };
@ -662,19 +662,19 @@ impl IdmServerProxyWriteTransaction<'_> {
&& eperm_rem_attested_passkeys; && eperm_rem_attested_passkeys;
let eperm_search_unixcred = match &eperm.search { let eperm_search_unixcred = match &eperm.search {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword), Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
}; };
let eperm_mod_unixcred = match &eperm.modify_pres { let eperm_mod_unixcred = match &eperm.modify_pres {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword), Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
}; };
let eperm_rem_unixcred = match &eperm.modify_rem { let eperm_rem_unixcred = match &eperm.modify_rem {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword), Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
}; };
@ -685,19 +685,19 @@ impl IdmServerProxyWriteTransaction<'_> {
&& eperm_rem_unixcred; && eperm_rem_unixcred;
let eperm_search_sshpubkey = match &eperm.search { let eperm_search_sshpubkey = match &eperm.search {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey), Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
}; };
let eperm_mod_sshpubkey = match &eperm.modify_pres { let eperm_mod_sshpubkey = match &eperm.modify_pres {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey), Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
}; };
let eperm_rem_sshpubkey = match &eperm.modify_rem { let eperm_rem_sshpubkey = match &eperm.modify_rem {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey), Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
}; };
@ -726,7 +726,7 @@ impl IdmServerProxyWriteTransaction<'_> {
})?; })?;
match &eperm.search { match &eperm.search {
Access::Denied => false, Access::Deny => false,
Access::Grant => true, Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SyncCredentialPortal), Access::Allow(attrs) => attrs.contains(&Attribute::SyncCredentialPortal),
} }

View file

@ -72,6 +72,8 @@ pub struct BuiltinAcp {
modify_present_attrs: Vec<Attribute>, modify_present_attrs: Vec<Attribute>,
modify_removed_attrs: Vec<Attribute>, modify_removed_attrs: Vec<Attribute>,
modify_classes: Vec<EntryClass>, modify_classes: Vec<EntryClass>,
modify_present_classes: Vec<EntryClass>,
modify_remove_classes: Vec<EntryClass>,
create_classes: Vec<EntryClass>, create_classes: Vec<EntryClass>,
create_attrs: Vec<Attribute>, create_attrs: Vec<Attribute>,
} }
@ -159,9 +161,19 @@ impl From<BuiltinAcp> for EntryInitNew {
value.modify_removed_attrs.into_iter().for_each(|attr| { value.modify_removed_attrs.into_iter().for_each(|attr| {
entry.add_ava(Attribute::AcpModifyRemovedAttr, Value::from(attr)); entry.add_ava(Attribute::AcpModifyRemovedAttr, Value::from(attr));
}); });
value.modify_classes.into_iter().for_each(|class| { value.modify_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpModifyClass, Value::from(class)); entry.add_ava(Attribute::AcpModifyClass, Value::from(class));
}); });
value.modify_present_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpModifyPresentClass, Value::from(class));
});
value.modify_remove_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpModifyRemoveClass, Value::from(class));
});
value.create_classes.into_iter().for_each(|class| { value.create_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpCreateClass, Value::from(class)); entry.add_ava(Attribute::AcpCreateClass, Value::from(class));
}); });
@ -214,7 +226,7 @@ lazy_static! {
ATTR_RECYCLED.to_string() ATTR_RECYCLED.to_string()
)), )),
modify_removed_attrs: vec![Attribute::Class], modify_removed_attrs: vec![Attribute::Class],
modify_classes: vec![EntryClass::Recycled], modify_remove_classes: vec![EntryClass::Recycled],
..Default::default() ..Default::default()
}; };
} }
@ -425,6 +437,7 @@ lazy_static! {
EntryClass::AccessControlCreate, EntryClass::AccessControlCreate,
EntryClass::AccessControlDelete, EntryClass::AccessControlDelete,
], ],
..Default::default()
}; };
} }

View file

@ -695,7 +695,6 @@ mod tests {
let e = entry_init!( let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")), (Attribute::Name, Value::new_iname("testperson")),
(Attribute::DisplayName, Value::new_iname("testperson")), (Attribute::DisplayName, Value::new_iname("testperson")),
( (
@ -726,7 +725,6 @@ mod tests {
let e = entry_init!( let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")), (Attribute::Name, Value::new_iname("testperson")),
(Attribute::DisplayName, Value::new_iname("testperson")), (Attribute::DisplayName, Value::new_iname("testperson")),
( (

View file

@ -22,7 +22,6 @@ mod jwskeygen;
mod keyobject; mod keyobject;
mod memberof; mod memberof;
mod namehistory; mod namehistory;
mod protected;
mod refint; mod refint;
mod session; mod session;
mod spn; mod spn;
@ -44,6 +43,7 @@ trait Plugin {
Err(OperationError::InvalidState) Err(OperationError::InvalidState)
} }
#[allow(dead_code)]
fn pre_create( fn pre_create(
_qs: &mut QueryServerWriteTransaction, _qs: &mut QueryServerWriteTransaction,
// List of what we will commit that is valid? // List of what we will commit that is valid?
@ -243,13 +243,13 @@ impl Plugins {
attrunique::AttrUnique::pre_create_transform(qs, cand, ce) attrunique::AttrUnique::pre_create_transform(qs, cand, ce)
} }
#[instrument(level = "debug", name = "plugins::run_pre_create", skip_all)] #[instrument(level = "trace", name = "plugins::run_pre_create", skip_all)]
pub fn run_pre_create( pub fn run_pre_create(
qs: &mut QueryServerWriteTransaction, _qs: &mut QueryServerWriteTransaction,
cand: &[Entry<EntrySealed, EntryNew>], _cand: &[Entry<EntrySealed, EntryNew>],
ce: &CreateEvent, _ce: &CreateEvent,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
protected::Protected::pre_create(qs, cand, ce) Ok(())
} }
#[instrument(level = "debug", name = "plugins::run_post_create", skip_all)] #[instrument(level = "debug", name = "plugins::run_post_create", skip_all)]
@ -269,7 +269,6 @@ impl Plugins {
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
me: &ModifyEvent, me: &ModifyEvent,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
protected::Protected::pre_modify(qs, pre_cand, cand, me)?;
base::Base::pre_modify(qs, pre_cand, cand, me)?; base::Base::pre_modify(qs, pre_cand, cand, me)?;
valuedeny::ValueDeny::pre_modify(qs, pre_cand, cand, me)?; valuedeny::ValueDeny::pre_modify(qs, pre_cand, cand, me)?;
cred_import::CredImport::pre_modify(qs, pre_cand, cand, me)?; cred_import::CredImport::pre_modify(qs, pre_cand, cand, me)?;
@ -305,7 +304,6 @@ impl Plugins {
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
me: &BatchModifyEvent, me: &BatchModifyEvent,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
protected::Protected::pre_batch_modify(qs, pre_cand, cand, me)?;
base::Base::pre_batch_modify(qs, pre_cand, cand, me)?; base::Base::pre_batch_modify(qs, pre_cand, cand, me)?;
valuedeny::ValueDeny::pre_batch_modify(qs, pre_cand, cand, me)?; valuedeny::ValueDeny::pre_batch_modify(qs, pre_cand, cand, me)?;
cred_import::CredImport::pre_batch_modify(qs, pre_cand, cand, me)?; cred_import::CredImport::pre_batch_modify(qs, pre_cand, cand, me)?;
@ -340,7 +338,6 @@ impl Plugins {
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
de: &DeleteEvent, de: &DeleteEvent,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
protected::Protected::pre_delete(qs, cand, de)?;
memberof::MemberOf::pre_delete(qs, cand, de) memberof::MemberOf::pre_delete(qs, cand, de)
} }

View file

@ -1,690 +0,0 @@
// System protected objects. Items matching specific requirements
// may only have certain modifications performed.
use hashbrown::HashSet;
use std::sync::Arc;
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent};
use crate::modify::Modify;
use crate::plugins::Plugin;
use crate::prelude::*;
pub struct Protected {}
// Here is the declaration of all the attrs that can be altered by
// a call on a system object. We trust they are allowed because
// schema will have checked this, and we don't allow class changes!
lazy_static! {
static ref ALLOWED_ATTRS: HashSet<Attribute> = {
let attrs = vec![
// Allow modification of some schema class types to allow local extension
// of schema types.
Attribute::Must,
Attribute::May,
// modification of some domain info types for local configuratiomn.
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::LdapAllowUnixPwBind,
Attribute::FernetPrivateKeyStr,
Attribute::Es256PrivateKeyDer,
Attribute::KeyActionRevoke,
Attribute::KeyActionRotate,
Attribute::IdVerificationEcKey,
Attribute::BadlistPassword,
Attribute::DeniedName,
Attribute::DomainDisplayName,
Attribute::Image,
// modification of account policy values for dyngroup.
Attribute::AuthSessionExpiry,
Attribute::AuthPasswordMinimumLength,
Attribute::CredentialTypeMinimum,
Attribute::PrivilegeExpiry,
Attribute::WebauthnAttestationCaList,
Attribute::LimitSearchMaxResults,
Attribute::LimitSearchMaxFilterTest,
Attribute::AllowPrimaryCredFallback,
];
let mut m = HashSet::with_capacity(attrs.len());
m.extend(attrs);
m
};
static ref PROTECTED_ENTRYCLASSES: Vec<EntryClass> =
vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
EntryClass::SyncObject,
EntryClass::Tombstone,
EntryClass::Recycled,
];
}
impl Plugin for Protected {
fn id() -> &'static str {
"plugin_protected"
}
#[instrument(level = "debug", name = "protected_pre_create", skip_all)]
fn pre_create(
_qs: &mut QueryServerWriteTransaction,
// List of what we will commit that is valid?
cand: &[Entry<EntrySealed, EntryNew>],
ce: &CreateEvent,
) -> Result<(), OperationError> {
if ce.ident.is_internal() {
trace!("Internal operation, not enforcing system object protection");
return Ok(());
}
cand.iter().try_fold((), |(), cand| {
if PROTECTED_ENTRYCLASSES
.iter()
.any(|c| cand.attribute_equality(Attribute::Class, &c.to_partialvalue()))
{
trace!("Rejecting operation during pre_create check");
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
})
}
#[instrument(level = "debug", name = "protected_pre_modify", skip_all)]
fn pre_modify(
_qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<EntryInvalidCommitted>,
me: &ModifyEvent,
) -> Result<(), OperationError> {
if me.ident.is_internal() {
trace!("Internal operation, not enforcing system object protection");
return Ok(());
}
// Prevent adding class: system, domain_info, tombstone, or recycled.
me.modlist.iter().try_fold((), |(), m| match m {
Modify::Present(a, v) => {
if a == Attribute::Class.as_ref()
&& PROTECTED_ENTRYCLASSES.iter().any(|c| v == &c.to_value())
{
trace!("Rejecting operation during pre_modify check");
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
}
_ => Ok(()),
})?;
// HARD block mods on tombstone or recycle. We soft block on the rest as they may
// have some allowed attrs.
cand.iter().try_fold((), |(), cand| {
if cand.attribute_equality(Attribute::Class, &EntryClass::Tombstone.into())
|| cand.attribute_equality(Attribute::Class, &EntryClass::Recycled.into())
{
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
})?;
// if class: system, check the mods are "allowed"
let system_pres = cand.iter().any(|c| {
// We don't need to check for domain info here because domain_info has a class
// system also. We just need to block it from being created.
c.attribute_equality(Attribute::Class, &EntryClass::System.into())
});
trace!("class: system -> {}", system_pres);
// No system types being altered, return.
if !system_pres {
return Ok(());
}
// Something altered is system, check if it's allowed.
me.modlist.into_iter().try_fold((), |(), m| {
// Already hit an error, move on.
let a = match m {
Modify::Present(a, _)
| Modify::Removed(a, _)
| Modify::Set(a, _)
| Modify::Purged(a) => Some(a),
Modify::Assert(_, _) => None,
};
if let Some(attr) = a {
match ALLOWED_ATTRS.contains(attr) {
true => Ok(()),
false => {
trace!("If you're getting this, you need to modify the ALLOWED_ATTRS list");
Err(OperationError::SystemProtectedObject)
}
}
} else {
// Was not a mod needing checking
Ok(())
}
})
}
#[instrument(level = "debug", name = "protected_pre_batch_modify", skip_all)]
fn pre_batch_modify(
_qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<EntryInvalidCommitted>,
me: &BatchModifyEvent,
) -> Result<(), OperationError> {
if me.ident.is_internal() {
trace!("Internal operation, not enforcing system object protection");
return Ok(());
}
me.modset
.values()
.flat_map(|ml| ml.iter())
.try_fold((), |(), m| match m {
Modify::Present(a, v) => {
if a == Attribute::Class.as_ref()
&& PROTECTED_ENTRYCLASSES.iter().any(|c| v == &c.to_value())
{
trace!("Rejecting operation during pre_batch_modify check");
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
}
_ => Ok(()),
})?;
// HARD block mods on tombstone or recycle. We soft block on the rest as they may
// have some allowed attrs.
cand.iter().try_fold((), |(), cand| {
if cand.attribute_equality(Attribute::Class, &EntryClass::Tombstone.into())
|| cand.attribute_equality(Attribute::Class, &EntryClass::Recycled.into())
{
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
})?;
// if class: system, check the mods are "allowed"
let system_pres = cand.iter().any(|c| {
// We don't need to check for domain info here because domain_info has a class
// system also. We just need to block it from being created.
c.attribute_equality(Attribute::Class, &EntryClass::System.into())
});
trace!("{}: system -> {}", Attribute::Class, system_pres);
// No system types being altered, return.
if !system_pres {
return Ok(());
}
// Something altered is system, check if it's allowed.
me.modset
.values()
.flat_map(|ml| ml.iter())
.try_fold((), |(), m| {
// Already hit an error, move on.
let a = match m {
Modify::Present(a, _) | Modify::Removed(a, _) | Modify::Set(a, _) | Modify::Purged(a) => Some(a),
Modify::Assert(_, _) => None,
};
if let Some(attr) = a {
match ALLOWED_ATTRS.contains(attr) {
true => Ok(()),
false => {
trace!("Rejecting operation during pre_batch_modify check, if you're getting this check ALLOWED_ATTRS");
Err(OperationError::SystemProtectedObject)
},
}
} else {
// Was not a mod needing checking
Ok(())
}
})
}
#[instrument(level = "debug", name = "protected_pre_delete", skip_all)]
fn pre_delete(
_qs: &mut QueryServerWriteTransaction,
// Should these be EntrySealed
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
de: &DeleteEvent,
) -> Result<(), OperationError> {
if de.ident.is_internal() {
trace!("Internal operation, not enforcing system object protection");
return Ok(());
}
cand.iter().try_fold((), |(), cand| {
if PROTECTED_ENTRYCLASSES
.iter()
.any(|c| cand.attribute_equality(Attribute::Class, &c.to_partialvalue()))
{
trace!("Rejecting operation during pre_delete check");
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
})
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use std::sync::Arc;
const UUID_TEST_ACCOUNT: Uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
const UUID_TEST_GROUP: Uuid = uuid::uuid!("81ec1640-3637-4a2f-8a52-874fa3c3c92f");
const UUID_TEST_ACP: Uuid = uuid::uuid!("acae81d6-5ea7-4bd8-8f7f-fcec4c0dd647");
lazy_static! {
pub static ref TEST_ACCOUNT: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::MemberOf.to_value()),
(Attribute::Name, Value::new_iname("test_account_1")),
(Attribute::DisplayName, Value::new_utf8s("test_account_1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT)),
(Attribute::MemberOf, Value::Refer(UUID_TEST_GROUP))
);
pub static ref TEST_GROUP: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("test_group_a")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP)),
(Attribute::Member, Value::Refer(UUID_TEST_ACCOUNT))
);
pub static ref ALLOW_ALL: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(
Attribute::Class,
EntryClass::AccessControlProfile.to_value()
),
(
Attribute::Class,
EntryClass::AccessControlTargetScope.to_value()
),
(
Attribute::Class,
EntryClass::AccessControlReceiverGroup.to_value()
),
(Attribute::Class, EntryClass::AccessControlModify.to_value()),
(Attribute::Class, EntryClass::AccessControlCreate.to_value()),
(Attribute::Class, EntryClass::AccessControlDelete.to_value()),
(Attribute::Class, EntryClass::AccessControlSearch.to_value()),
(
Attribute::Name,
Value::new_iname("idm_admins_acp_allow_all_test")
),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACP)),
(Attribute::AcpReceiverGroup, Value::Refer(UUID_TEST_GROUP)),
(
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"pres\":\"class\"}").expect("filter")
),
(Attribute::AcpSearchAttr, Value::from(Attribute::Name)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Class)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Uuid)),
(Attribute::AcpSearchAttr, Value::new_iutf8("classname")),
(
Attribute::AcpSearchAttr,
Value::new_iutf8(Attribute::AttributeName.as_ref())
),
(Attribute::AcpModifyClass, EntryClass::System.to_value()),
(Attribute::AcpModifyClass, Value::new_iutf8("domain_info")),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyRemovedAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Must)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DomainName)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DomainDisplayName)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DomainUuid)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DomainSsid)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::FernetPrivateKeyStr)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Es256PrivateKeyDer)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::PrivateCookieKey)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyPresentAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Must)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DomainName)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DomainDisplayName)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DomainUuid)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DomainSsid)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::FernetPrivateKeyStr)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Es256PrivateKeyDer)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::PrivateCookieKey)
),
(Attribute::AcpCreateClass, EntryClass::Object.to_value()),
(Attribute::AcpCreateClass, EntryClass::Account.to_value()),
(Attribute::AcpCreateClass, EntryClass::Person.to_value()),
(Attribute::AcpCreateClass, EntryClass::System.to_value()),
(Attribute::AcpCreateClass, EntryClass::DomainInfo.to_value()),
(Attribute::AcpCreateAttr, Value::from(Attribute::Name)),
(Attribute::AcpCreateAttr, EntryClass::Class.to_value(),),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::Description),
),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::DisplayName),
),
(Attribute::AcpCreateAttr, Value::from(Attribute::DomainName),),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::DomainDisplayName)
),
(Attribute::AcpCreateAttr, Value::from(Attribute::DomainUuid)),
(Attribute::AcpCreateAttr, Value::from(Attribute::DomainSsid)),
(Attribute::AcpCreateAttr, Value::from(Attribute::Uuid)),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::FernetPrivateKeyStr)
),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::Es256PrivateKeyDer)
),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::PrivateCookieKey)
),
(Attribute::AcpCreateAttr, Value::from(Attribute::Version))
);
pub static ref PRELOAD: Vec<EntryInitNew> =
vec![TEST_ACCOUNT.clone(), TEST_GROUP.clone(), ALLOW_ALL.clone()];
pub static ref E_TEST_ACCOUNT: Arc<EntrySealedCommitted> =
Arc::new(TEST_ACCOUNT.clone().into_sealed_committed());
}
#[test]
fn test_pre_create_deny() {
// Test creating with class: system is rejected.
let e = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
)
);
let create = vec![e];
let preload = PRELOAD.clone();
run_create_test!(
Err(OperationError::SystemProtectedObject),
preload,
create,
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
#[test]
fn test_pre_modify_system_deny() {
// Test modify of class to a system is denied
let e = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
)
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_modify_test!(
Err(OperationError::SystemProtectedObject),
preload,
filter!(f_eq(Attribute::Name, PartialValue::new_iname("testperson"))),
modlist!([
m_purge(Attribute::DisplayName),
m_pres(Attribute::DisplayName, &Value::new_utf8s("system test")),
]),
Some(E_TEST_ACCOUNT.clone()),
|_| {},
|_| {}
);
}
#[test]
fn test_pre_modify_class_add_deny() {
// Show that adding a system class is denied
// TODO: replace this with a `SchemaClass` object
let e = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ClassType.to_value()),
(Attribute::ClassName, Value::new_iutf8("testclass")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("66c68b2f-d02c-4243-8013-7946e40fe321"))
),
(
Attribute::Description,
Value::Utf8("class test".to_string())
)
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_modify_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::ClassName,
PartialValue::new_iutf8("testclass")
)),
modlist!([
m_pres(Attribute::May, &Value::from(Attribute::Name)),
m_pres(Attribute::Must, &Value::from(Attribute::Name)),
]),
Some(E_TEST_ACCOUNT.clone()),
|_| {},
|_| {}
);
}
#[test]
fn test_pre_delete_deny() {
// Test deleting with class: system is rejected.
let e = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
)
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_delete_test!(
Err(OperationError::SystemProtectedObject),
preload,
filter!(f_eq(Attribute::Name, PartialValue::new_iname("testperson"))),
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
#[test]
fn test_modify_domain() {
// Can edit *my* domain_ssid and domain_name
// Show that adding a system class is denied
let e = entry_init!(
(Attribute::Class, EntryClass::DomainInfo.to_value()),
(Attribute::Name, Value::new_iname("domain_example.net.au")),
(Attribute::Uuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(
Attribute::Description,
Value::new_utf8s("Demonstration of a remote domain's info being created for uuid generation in test_modify_domain")
),
(Attribute::DomainUuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(Attribute::DomainName, Value::new_iname("example.net.au")),
(Attribute::DomainDisplayName, Value::Utf8("example.net.au".to_string())),
(Attribute::DomainSsid, Value::Utf8("Example_Wifi".to_string())),
(Attribute::Version, Value::Uint32(1))
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_modify_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("domain_example.net.au")
)),
modlist!([
m_purge(Attribute::DomainSsid),
m_pres(Attribute::DomainSsid, &Value::new_utf8s("NewExampleWifi")),
]),
Some(E_TEST_ACCOUNT.clone()),
|_| {},
|_| {}
);
}
#[test]
fn test_ext_create_domain() {
// can not add a domain_info type - note the lack of class: system
let e = entry_init!(
(Attribute::Class, EntryClass::DomainInfo.to_value()),
(Attribute::Name, Value::new_iname("domain_example.net.au")),
(Attribute::Uuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(
Attribute::Description,
Value::new_utf8s("Demonstration of a remote domain's info being created for uuid generation in test_modify_domain")
),
(Attribute::DomainUuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(Attribute::DomainName, Value::new_iname("example.net.au")),
(Attribute::DomainDisplayName, Value::Utf8("example.net.au".to_string())),
(Attribute::DomainSsid, Value::Utf8("Example_Wifi".to_string())),
(Attribute::Version, Value::Uint32(1))
);
let create = vec![e];
let preload = PRELOAD.clone();
run_create_test!(
Err(OperationError::SystemProtectedObject),
preload,
create,
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
#[test]
fn test_delete_domain() {
// On the real thing we have a class: system, but to prove the point ...
let e = entry_init!(
(Attribute::Class, EntryClass::DomainInfo.to_value()),
(Attribute::Name, Value::new_iname("domain_example.net.au")),
(Attribute::Uuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(
Attribute::Description,
Value::new_utf8s("Demonstration of a remote domain's info being created for uuid generation in test_modify_domain")
),
(Attribute::DomainUuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(Attribute::DomainName, Value::new_iname("example.net.au")),
(Attribute::DomainDisplayName, Value::Utf8("example.net.au".to_string())),
(Attribute::DomainSsid, Value::Utf8("Example_Wifi".to_string())),
(Attribute::Version, Value::Uint32(1))
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_delete_test!(
Err(OperationError::SystemProtectedObject),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("domain_example.net.au")
)),
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
}

View file

@ -1366,6 +1366,36 @@ impl SchemaWriteTransaction<'_> {
syntax: SyntaxType::Utf8StringInsensitive, syntax: SyntaxType::Utf8StringInsensitive,
}, },
); );
self.attributes.insert(
Attribute::AcpModifyPresentClass,
SchemaAttribute {
name: Attribute::AcpModifyPresentClass,
uuid: UUID_SCHEMA_ATTR_ACP_MODIFY_PRESENT_CLASS,
description: String::from("The set of class values that could be asserted or added to an entry. Only applies to modify::present operations on class."),
multivalue: true,
unique: false,
phantom: false,
sync_allowed: false,
replicated: Replicated::True,
indexed: false,
syntax: SyntaxType::Utf8StringInsensitive,
},
);
self.attributes.insert(
Attribute::AcpModifyRemoveClass,
SchemaAttribute {
name: Attribute::AcpModifyRemoveClass,
uuid: UUID_SCHEMA_ATTR_ACP_MODIFY_REMOVE_CLASS,
description: String::from("The set of class values that could be asserted or added to an entry. Only applies to modify::remove operations on class."),
multivalue: true,
unique: false,
phantom: false,
sync_allowed: false,
replicated: Replicated::True,
indexed: false,
syntax: SyntaxType::Utf8StringInsensitive,
},
);
self.attributes.insert( self.attributes.insert(
Attribute::EntryManagedBy, Attribute::EntryManagedBy,
SchemaAttribute { SchemaAttribute {
@ -2069,6 +2099,8 @@ impl SchemaWriteTransaction<'_> {
Attribute::AcpModifyRemovedAttr, Attribute::AcpModifyRemovedAttr,
Attribute::AcpModifyPresentAttr, Attribute::AcpModifyPresentAttr,
Attribute::AcpModifyClass, Attribute::AcpModifyClass,
Attribute::AcpModifyPresentClass,
Attribute::AcpModifyRemoveClass,
], ],
..Default::default() ..Default::default()
}, },

View file

@ -1,16 +1,17 @@
use super::profiles::{ use super::profiles::{
AccessControlCreateResolved, AccessControlReceiverCondition, AccessControlTargetCondition, AccessControlCreateResolved, AccessControlReceiverCondition, AccessControlTargetCondition,
}; };
use super::protected::PROTECTED_ENTRY_CLASSES;
use crate::prelude::*; use crate::prelude::*;
use std::collections::BTreeSet; use std::collections::BTreeSet;
pub(super) enum CreateResult { pub(super) enum CreateResult {
Denied, Deny,
Grant, Grant,
} }
enum IResult { enum IResult {
Denied, Deny,
Grant, Grant,
Ignore, Ignore,
} }
@ -25,25 +26,25 @@ pub(super) fn apply_create_access<'a>(
// This module can never yield a grant. // This module can never yield a grant.
match protected_filter_entry(ident, entry) { match protected_filter_entry(ident, entry) {
IResult::Denied => denied = true, IResult::Deny => denied = true,
IResult::Grant | IResult::Ignore => {} IResult::Grant | IResult::Ignore => {}
} }
match create_filter_entry(ident, related_acp, entry) { match create_filter_entry(ident, related_acp, entry) {
IResult::Denied => denied = true, IResult::Deny => denied = true,
IResult::Grant => grant = true, IResult::Grant => grant = true,
IResult::Ignore => {} IResult::Ignore => {}
} }
if denied { if denied {
// Something explicitly said no. // Something explicitly said no.
CreateResult::Denied CreateResult::Deny
} else if grant { } else if grant {
// Something said yes // Something said yes
CreateResult::Grant CreateResult::Grant
} else { } else {
// Nothing said yes. // Nothing said yes.
CreateResult::Denied CreateResult::Deny
} }
} }
@ -60,7 +61,7 @@ fn create_filter_entry<'a>(
} }
IdentType::Synch(_) => { IdentType::Synch(_) => {
security_critical!("Blocking sync check"); security_critical!("Blocking sync check");
return IResult::Denied; return IResult::Deny;
} }
IdentType::User(_) => {} IdentType::User(_) => {}
}; };
@ -69,7 +70,7 @@ fn create_filter_entry<'a>(
match ident.access_scope() { match ident.access_scope() {
AccessScope::ReadOnly | AccessScope::Synchronise => { AccessScope::ReadOnly | AccessScope::Synchronise => {
security_access!("denied ❌ - identity access scope is not permitted to create"); security_access!("denied ❌ - identity access scope is not permitted to create");
return IResult::Denied; return IResult::Deny;
} }
AccessScope::ReadWrite => { AccessScope::ReadWrite => {
// As you were // As you were
@ -96,7 +97,7 @@ fn create_filter_entry<'a>(
Some(s) => s.collect(), Some(s) => s.collect(),
None => { None => {
admin_error!("Class set failed to build - corrupted entry?"); admin_error!("Class set failed to build - corrupted entry?");
return IResult::Denied; return IResult::Deny;
} }
}; };
@ -173,22 +174,22 @@ fn protected_filter_entry(ident: &Identity, entry: &Entry<EntryInit, EntryNew>)
} }
IdentType::Synch(_) => { IdentType::Synch(_) => {
security_access!("sync agreements may not directly create entities"); security_access!("sync agreements may not directly create entities");
IResult::Denied IResult::Deny
} }
IdentType::User(_) => { IdentType::User(_) => {
// Now check things ... // Now check things ...
if let Some(classes) = entry.get_ava_as_iutf8(Attribute::Class) {
// For now we just block create on sync object if classes.is_disjoint(&PROTECTED_ENTRY_CLASSES) {
if let Some(classes) = entry.get_ava_set(Attribute::Class) { // It's different, go ahead
if classes.contains(&EntryClass::SyncObject.into()) {
// Block the mod
security_access!("attempt to create with protected class type");
IResult::Denied
} else {
IResult::Ignore IResult::Ignore
} else {
// Block the mod, something is present
security_access!("attempt to create with protected class type");
IResult::Deny
} }
} else { } else {
// Nothing to check. // Nothing to check - this entry will fail to create anyway because it has
// no classes
IResult::Ignore IResult::Ignore
} }
} }

View file

@ -1,16 +1,17 @@
use super::profiles::{ use super::profiles::{
AccessControlDeleteResolved, AccessControlReceiverCondition, AccessControlTargetCondition, AccessControlDeleteResolved, AccessControlReceiverCondition, AccessControlTargetCondition,
}; };
use super::protected::PROTECTED_ENTRY_CLASSES;
use crate::prelude::*; use crate::prelude::*;
use std::sync::Arc; use std::sync::Arc;
pub(super) enum DeleteResult { pub(super) enum DeleteResult {
Denied, Deny,
Grant, Grant,
} }
enum IResult { enum IResult {
Denied, Deny,
Grant, Grant,
Ignore, Ignore,
} }
@ -24,25 +25,25 @@ pub(super) fn apply_delete_access<'a>(
let mut grant = false; let mut grant = false;
match protected_filter_entry(ident, entry) { match protected_filter_entry(ident, entry) {
IResult::Denied => denied = true, IResult::Deny => denied = true,
IResult::Grant | IResult::Ignore => {} IResult::Grant | IResult::Ignore => {}
} }
match delete_filter_entry(ident, related_acp, entry) { match delete_filter_entry(ident, related_acp, entry) {
IResult::Denied => denied = true, IResult::Deny => denied = true,
IResult::Grant => grant = true, IResult::Grant => grant = true,
IResult::Ignore => {} IResult::Ignore => {}
} }
if denied { if denied {
// Something explicitly said no. // Something explicitly said no.
DeleteResult::Denied DeleteResult::Deny
} else if grant { } else if grant {
// Something said yes // Something said yes
DeleteResult::Grant DeleteResult::Grant
} else { } else {
// Nothing said yes. // Nothing said yes.
DeleteResult::Denied DeleteResult::Deny
} }
} }
@ -59,7 +60,7 @@ fn delete_filter_entry<'a>(
} }
IdentType::Synch(_) => { IdentType::Synch(_) => {
security_critical!("Blocking sync check"); security_critical!("Blocking sync check");
return IResult::Denied; return IResult::Deny;
} }
IdentType::User(_) => {} IdentType::User(_) => {}
}; };
@ -68,7 +69,7 @@ fn delete_filter_entry<'a>(
match ident.access_scope() { match ident.access_scope() {
AccessScope::ReadOnly | AccessScope::Synchronise => { AccessScope::ReadOnly | AccessScope::Synchronise => {
security_access!("denied ❌ - identity access scope is not permitted to delete"); security_access!("denied ❌ - identity access scope is not permitted to delete");
return IResult::Denied; return IResult::Deny;
} }
AccessScope::ReadWrite => { AccessScope::ReadWrite => {
// As you were // As you were
@ -152,28 +153,30 @@ fn protected_filter_entry(ident: &Identity, entry: &Arc<EntrySealedCommitted>) -
} }
IdentType::Synch(_) => { IdentType::Synch(_) => {
security_access!("sync agreements may not directly delete entities"); security_access!("sync agreements may not directly delete entities");
IResult::Denied IResult::Deny
} }
IdentType::User(_) => { IdentType::User(_) => {
// Now check things ...
// For now we just block create on sync object
if let Some(classes) = entry.get_ava_set(Attribute::Class) {
if classes.contains(&EntryClass::SyncObject.into()) {
// Block the mod
security_access!("attempt to delete with protected class type");
return IResult::Denied;
}
};
// Prevent deletion of entries that exist in the system controlled entry range. // Prevent deletion of entries that exist in the system controlled entry range.
if entry.get_uuid() <= UUID_ANONYMOUS { if entry.get_uuid() <= UUID_ANONYMOUS {
security_access!("attempt to delete system builtin entry"); security_access!("attempt to delete system builtin entry");
return IResult::Denied; return IResult::Deny;
} }
// Checks exhausted, no more input from us // Prevent deleting some protected types.
IResult::Ignore if let Some(classes) = entry.get_ava_as_iutf8(Attribute::Class) {
if classes.is_disjoint(&PROTECTED_ENTRY_CLASSES) {
// It's different, go ahead
IResult::Ignore
} else {
// Block the mod, something is present
security_access!("attempt to create with protected class type");
IResult::Deny
}
} else {
// Nothing to check - this entry will fail to create anyway because it has
// no classes
IResult::Ignore
}
} }
} }
} }

View file

@ -50,12 +50,13 @@ mod create;
mod delete; mod delete;
mod modify; mod modify;
pub mod profiles; pub mod profiles;
mod protected;
mod search; mod search;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Access { pub enum Access {
Grant, Grant,
Denied, Deny,
Allow(BTreeSet<Attribute>), Allow(BTreeSet<Attribute>),
} }
@ -63,7 +64,7 @@ impl From<&Access> for ScimAttributeEffectiveAccess {
fn from(value: &Access) -> Self { fn from(value: &Access) -> Self {
match value { match value {
Access::Grant => Self::Grant, Access::Grant => Self::Grant,
Access::Denied => Self::Denied, Access::Deny => Self::Deny,
Access::Allow(set) => Self::Allow(set.clone()), Access::Allow(set) => Self::Allow(set.clone()),
} }
} }
@ -72,7 +73,7 @@ impl From<&Access> for ScimAttributeEffectiveAccess {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum AccessClass { pub enum AccessClass {
Grant, Grant,
Denied, Deny,
Allow(BTreeSet<AttrString>), Allow(BTreeSet<AttrString>),
} }
@ -86,12 +87,22 @@ pub struct AccessEffectivePermission {
pub search: Access, pub search: Access,
pub modify_pres: Access, pub modify_pres: Access,
pub modify_rem: Access, pub modify_rem: Access,
pub modify_class: AccessClass, pub modify_pres_class: AccessClass,
pub modify_rem_class: AccessClass,
} }
pub enum AccessResult { pub enum AccessBasicResult {
// Deny this operation unconditionally. // Deny this operation unconditionally.
Denied, Deny,
// Unbounded allow, provided no deny state exists.
Grant,
// This module makes no decisions about this entry.
Ignore,
}
pub enum AccessSrchResult {
// Deny this operation unconditionally.
Deny,
// Unbounded allow, provided no deny state exists. // Unbounded allow, provided no deny state exists.
Grant, Grant,
// This module makes no decisions about this entry. // This module makes no decisions about this entry.
@ -99,24 +110,37 @@ pub enum AccessResult {
// Limit the allowed attr set to this - this doesn't // Limit the allowed attr set to this - this doesn't
// allow anything, it constrains what might be allowed // allow anything, it constrains what might be allowed
// by a later module. // by a later module.
Constrain(BTreeSet<Attribute>), /*
// Allow these attributes within constraints. Constrain {
Allow(BTreeSet<Attribute>), attr: BTreeSet<Attribute>,
},
*/
Allow { attr: BTreeSet<Attribute> },
} }
#[allow(dead_code)] pub enum AccessModResult<'a> {
pub enum AccessResultClass<'a> {
// Deny this operation unconditionally. // Deny this operation unconditionally.
Denied, Deny,
// Unbounded allow, provided no denied exists. // Unbounded allow, provided no deny state exists.
Grant, // Grant,
// This module makes no decisions about this entry. // This module makes no decisions about this entry.
Ignore, Ignore,
// Limit the allowed attr set to this - this doesn't // Limit the allowed attr set to this - this doesn't
// allow anything, it constrains what might be allowed. // allow anything, it constrains what might be allowed
Constrain(BTreeSet<&'a str>), // by a later module.
// Allow these attributes within constraints. Constrain {
Allow(BTreeSet<&'a str>), pres_attr: BTreeSet<Attribute>,
rem_attr: BTreeSet<Attribute>,
pres_cls: Option<BTreeSet<&'a str>>,
rem_cls: Option<BTreeSet<&'a str>>,
},
// Allow these modifications within constraints.
Allow {
pres_attr: BTreeSet<Attribute>,
rem_attr: BTreeSet<Attribute>,
pres_class: BTreeSet<&'a str>,
rem_class: BTreeSet<&'a str>,
},
} }
// ========================================================================= // =========================================================================
@ -303,7 +327,7 @@ pub trait AccessControlsTransaction<'a> {
.into_iter() .into_iter()
.filter(|e| { .filter(|e| {
match apply_search_access(ident, related_acp.as_slice(), e) { match apply_search_access(ident, related_acp.as_slice(), e) {
SearchResult::Denied => false, SearchResult::Deny => false,
SearchResult::Grant => true, SearchResult::Grant => true,
SearchResult::Allow(allowed_attrs) => { SearchResult::Allow(allowed_attrs) => {
// The allow set constrained. // The allow set constrained.
@ -401,7 +425,7 @@ pub trait AccessControlsTransaction<'a> {
.into_iter() .into_iter()
.filter_map(|entry| { .filter_map(|entry| {
match apply_search_access(&se.ident, &search_related_acp, &entry) { match apply_search_access(&se.ident, &search_related_acp, &entry) {
SearchResult::Denied => { SearchResult::Deny => {
None None
} }
SearchResult::Grant => { SearchResult::Grant => {
@ -536,7 +560,8 @@ pub trait AccessControlsTransaction<'a> {
// Build the set of classes that we to work on, only in terms of "addition". To remove // Build the set of classes that we to work on, only in terms of "addition". To remove
// I think we have no limit, but ... william of the future may find a problem with this // I think we have no limit, but ... william of the future may find a problem with this
// policy. // policy.
let mut requested_classes: BTreeSet<&str> = Default::default(); let mut requested_pres_classes: BTreeSet<&str> = Default::default();
let mut requested_rem_classes: BTreeSet<&str> = Default::default();
for modify in me.modlist.iter() { for modify in me.modlist.iter() {
match modify { match modify {
@ -548,27 +573,33 @@ pub trait AccessControlsTransaction<'a> {
// existence, and second, we would have failed the mod at schema checking // existence, and second, we would have failed the mod at schema checking
// earlier in the process as these were not correctly type. As a result // earlier in the process as these were not correctly type. As a result
// we can trust these to be correct here and not to be "None". // we can trust these to be correct here and not to be "None".
requested_classes.extend(v.to_str()) requested_pres_classes.extend(v.to_str())
} }
} }
Modify::Removed(a, v) => { Modify::Removed(a, v) => {
if a == Attribute::Class.as_ref() { if a == Attribute::Class.as_ref() {
requested_classes.extend(v.to_str()) requested_rem_classes.extend(v.to_str())
} }
} }
Modify::Set(a, v) => { Modify::Set(a, v) => {
if a == Attribute::Class.as_ref() { if a == Attribute::Class.as_ref() {
// flatten to remove the option down to an iterator // This is a reasonably complex case - we actually have to contemplate
requested_classes.extend(v.as_iutf8_iter().into_iter().flatten()) // the difference between what exists and what doesn't, but that's per-entry.
//
// for now, we treat this as both pres and rem, but I think that ultimately
// to fix this we need to make all modifies apply in terms of "batch mod"
requested_pres_classes.extend(v.as_iutf8_iter().into_iter().flatten());
requested_rem_classes.extend(v.as_iutf8_iter().into_iter().flatten());
} }
} }
_ => {} _ => {}
} }
} }
debug!(?requested_pres, "Requested present set"); debug!(?requested_pres, "Requested present attribute set");
debug!(?requested_rem, "Requested remove set"); debug!(?requested_rem, "Requested remove attribute set");
debug!(?requested_classes, "Requested class set"); debug!(?requested_pres_classes, "Requested present class set");
debug!(?requested_rem_classes, "Requested remove class set");
let sync_agmts = self.get_sync_agreements(); let sync_agmts = self.get_sync_agreements();
@ -576,9 +607,16 @@ pub trait AccessControlsTransaction<'a> {
debug!(entry_id = %e.get_display_id()); debug!(entry_id = %e.get_display_id());
match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) { match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) {
ModifyResult::Denied => false, ModifyResult::Deny => false,
ModifyResult::Grant => true, ModifyResult::Grant => true,
ModifyResult::Allow { pres, rem, cls } => { ModifyResult::Allow {
pres,
rem,
pres_cls,
rem_cls,
} => {
let mut decision = true;
if !requested_pres.is_subset(&pres) { if !requested_pres.is_subset(&pres) {
security_error!("requested_pres is not a subset of allowed"); security_error!("requested_pres is not a subset of allowed");
security_error!( security_error!(
@ -586,23 +624,41 @@ pub trait AccessControlsTransaction<'a> {
requested_pres, requested_pres,
pres pres
); );
false decision = false
} else if !requested_rem.is_subset(&rem) { };
if !requested_rem.is_subset(&rem) {
security_error!("requested_rem is not a subset of allowed"); security_error!("requested_rem is not a subset of allowed");
security_error!("requested_rem: {:?} !⊆ allowed: {:?}", requested_rem, rem); security_error!("requested_rem: {:?} !⊆ allowed: {:?}", requested_rem, rem);
false decision = false;
} else if !requested_classes.is_subset(&cls) { };
security_error!("requested_classes is not a subset of allowed");
if !requested_pres_classes.is_subset(&pres_cls) {
security_error!("requested_pres_classes is not a subset of allowed");
security_error!( security_error!(
"requested_classes: {:?} !⊆ allowed: {:?}", "requested_pres_classes: {:?} !⊆ allowed: {:?}",
requested_classes, requested_pres_classes,
cls pres_cls
); );
false decision = false;
} else { };
if !requested_rem_classes.is_subset(&rem_cls) {
security_error!("requested_rem_classes is not a subset of allowed");
security_error!(
"requested_rem_classes: {:?} !⊆ allowed: {:?}",
requested_rem_classes,
rem_cls
);
decision = false;
}
if decision {
debug!("passed pres, rem, classes check."); debug!("passed pres, rem, classes check.");
true }
} // if acc == false
// Yield the result
decision
} }
} }
}); });
@ -668,47 +724,55 @@ pub trait AccessControlsTransaction<'a> {
}) })
.collect(); .collect();
// Build the set of classes that we to work on, only in terms of "addition". To remove let mut requested_pres_classes: BTreeSet<&str> = Default::default();
// I think we have no limit, but ... william of the future may find a problem with this let mut requested_rem_classes: BTreeSet<&str> = Default::default();
// policy.
let requested_classes: BTreeSet<&str> = modlist for modify in modlist.iter() {
.iter() match modify {
.filter_map(|m| match m {
Modify::Present(a, v) => { Modify::Present(a, v) => {
if a == Attribute::Class.as_ref() { if a == Attribute::Class.as_ref() {
// Here we have an option<&str> which could mean there is a risk of requested_pres_classes.extend(v.to_str())
// a malicious entity attempting to trick us by masking class mods
// in non-iutf8 types. However, the server first won't respect their
// existence, and second, we would have failed the mod at schema checking
// earlier in the process as these were not correctly type. As a result
// we can trust these to be correct here and not to be "None".
v.to_str()
} else {
None
} }
} }
Modify::Removed(a, v) => { Modify::Removed(a, v) => {
if a == Attribute::Class.as_ref() { if a == Attribute::Class.as_ref() {
v.to_str() requested_rem_classes.extend(v.to_str())
} else {
None
} }
} }
_ => None, Modify::Set(a, v) => {
}) if a == Attribute::Class.as_ref() {
.collect(); // This is a reasonably complex case - we actually have to contemplate
// the difference between what exists and what doesn't, but that's per-entry.
//
// for now, we treat this as both pres and rem, but I think that ultimately
// to fix this we need to make all modifies apply in terms of "batch mod"
requested_pres_classes.extend(v.as_iutf8_iter().into_iter().flatten());
requested_rem_classes.extend(v.as_iutf8_iter().into_iter().flatten());
}
}
_ => {}
}
}
debug!(?requested_pres, "Requested present set"); debug!(?requested_pres, "Requested present set");
debug!(?requested_rem, "Requested remove set"); debug!(?requested_rem, "Requested remove set");
debug!(?requested_classes, "Requested class set"); debug!(?requested_pres_classes, "Requested present class set");
debug!(?requested_rem_classes, "Requested remove class set");
debug!(entry_id = %e.get_display_id()); debug!(entry_id = %e.get_display_id());
let sync_agmts = self.get_sync_agreements(); let sync_agmts = self.get_sync_agreements();
match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) { match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) {
ModifyResult::Denied => false, ModifyResult::Deny => false,
ModifyResult::Grant => true, ModifyResult::Grant => true,
ModifyResult::Allow { pres, rem, cls } => { ModifyResult::Allow {
pres,
rem,
pres_cls,
rem_cls,
} => {
let mut decision = true;
if !requested_pres.is_subset(&pres) { if !requested_pres.is_subset(&pres) {
security_error!("requested_pres is not a subset of allowed"); security_error!("requested_pres is not a subset of allowed");
security_error!( security_error!(
@ -716,23 +780,41 @@ pub trait AccessControlsTransaction<'a> {
requested_pres, requested_pres,
pres pres
); );
false decision = false
} else if !requested_rem.is_subset(&rem) { };
if !requested_rem.is_subset(&rem) {
security_error!("requested_rem is not a subset of allowed"); security_error!("requested_rem is not a subset of allowed");
security_error!("requested_rem: {:?} !⊆ allowed: {:?}", requested_rem, rem); security_error!("requested_rem: {:?} !⊆ allowed: {:?}", requested_rem, rem);
false decision = false;
} else if !requested_classes.is_subset(&cls) { };
security_error!("requested_classes is not a subset of allowed");
if !requested_pres_classes.is_subset(&pres_cls) {
security_error!("requested_pres_classes is not a subset of allowed");
security_error!( security_error!(
"requested_classes: {:?} !⊆ allowed: {:?}", "requested_classes: {:?} !⊆ allowed: {:?}",
requested_classes, requested_pres_classes,
cls pres_cls
); );
false decision = false;
} else { };
security_access!("passed pres, rem, classes check.");
true if !requested_rem_classes.is_subset(&rem_cls) {
} // if acc == false security_error!("requested_rem_classes is not a subset of allowed");
security_error!(
"requested_classes: {:?} !⊆ allowed: {:?}",
requested_rem_classes,
rem_cls
);
decision = false;
}
if decision {
debug!("passed pres, rem, classes check.");
}
// Yield the result
decision
} }
} }
}); });
@ -780,7 +862,7 @@ pub trait AccessControlsTransaction<'a> {
// For each entry // For each entry
let r = entries.iter().all(|e| { let r = entries.iter().all(|e| {
match apply_create_access(&ce.ident, related_acp.as_slice(), e) { match apply_create_access(&ce.ident, related_acp.as_slice(), e) {
CreateResult::Denied => false, CreateResult::Deny => false,
CreateResult::Grant => true, CreateResult::Grant => true,
} }
}); });
@ -836,7 +918,7 @@ pub trait AccessControlsTransaction<'a> {
// For each entry // For each entry
let r = entries.iter().all(|e| { let r = entries.iter().all(|e| {
match apply_delete_access(&de.ident, related_acp.as_slice(), e) { match apply_delete_access(&de.ident, related_acp.as_slice(), e) {
DeleteResult::Denied => false, DeleteResult::Deny => false,
DeleteResult::Grant => true, DeleteResult::Grant => true,
} }
}); });
@ -925,7 +1007,7 @@ pub trait AccessControlsTransaction<'a> {
) -> AccessEffectivePermission { ) -> AccessEffectivePermission {
// == search == // == search ==
let search_effective = match apply_search_access(ident, search_related_acp, entry) { let search_effective = match apply_search_access(ident, search_related_acp, entry) {
SearchResult::Denied => Access::Denied, SearchResult::Deny => Access::Deny,
SearchResult::Grant => Access::Grant, SearchResult::Grant => Access::Grant,
SearchResult::Allow(allowed_attrs) => { SearchResult::Allow(allowed_attrs) => {
// Bound by requested attrs? // Bound by requested attrs?
@ -934,14 +1016,30 @@ pub trait AccessControlsTransaction<'a> {
}; };
// == modify == // == modify ==
let (modify_pres, modify_rem, modify_class) = let (modify_pres, modify_rem, modify_pres_class, modify_rem_class) =
match apply_modify_access(ident, modify_related_acp, sync_agmts, entry) { match apply_modify_access(ident, modify_related_acp, sync_agmts, entry) {
ModifyResult::Denied => (Access::Denied, Access::Denied, AccessClass::Denied), ModifyResult::Deny => (
ModifyResult::Grant => (Access::Grant, Access::Grant, AccessClass::Grant), Access::Deny,
ModifyResult::Allow { pres, rem, cls } => ( Access::Deny,
AccessClass::Deny,
AccessClass::Deny,
),
ModifyResult::Grant => (
Access::Grant,
Access::Grant,
AccessClass::Grant,
AccessClass::Grant,
),
ModifyResult::Allow {
pres,
rem,
pres_cls,
rem_cls,
} => (
Access::Allow(pres.into_iter().collect()), Access::Allow(pres.into_iter().collect()),
Access::Allow(rem.into_iter().collect()), Access::Allow(rem.into_iter().collect()),
AccessClass::Allow(cls.into_iter().map(|s| s.into()).collect()), AccessClass::Allow(pres_cls.into_iter().map(|s| s.into()).collect()),
AccessClass::Allow(rem_cls.into_iter().map(|s| s.into()).collect()),
), ),
}; };
@ -949,7 +1047,7 @@ pub trait AccessControlsTransaction<'a> {
let delete_status = apply_delete_access(ident, delete_related_acp, entry); let delete_status = apply_delete_access(ident, delete_related_acp, entry);
let delete = match delete_status { let delete = match delete_status {
DeleteResult::Denied => false, DeleteResult::Deny => false,
DeleteResult::Grant => true, DeleteResult::Grant => true,
}; };
@ -960,7 +1058,8 @@ pub trait AccessControlsTransaction<'a> {
search: search_effective, search: search_effective,
modify_pres, modify_pres,
modify_rem, modify_rem,
modify_class, modify_pres_class,
modify_rem_class,
} }
} }
} }
@ -2166,6 +2265,8 @@ mod tests {
"name class", "name class",
// And the class allowed is account // And the class allowed is account
EntryClass::Account.into(), EntryClass::Account.into(),
// And the class allowed is account
EntryClass::Account.into(),
); );
// Allow member, class is group. IE not account // Allow member, class is group. IE not account
let acp_deny = AccessControlModify::from_raw( let acp_deny = AccessControlModify::from_raw(
@ -2182,8 +2283,8 @@ mod tests {
"member class", "member class",
// Allow rem name and class // Allow rem name and class
"member class", "member class",
// And the class allowed is account EntryClass::Group.into(),
"group", EntryClass::Group.into(),
); );
// Does not have a pres or rem class in attrs // Does not have a pres or rem class in attrs
let acp_no_class = AccessControlModify::from_raw( let acp_no_class = AccessControlModify::from_raw(
@ -2201,7 +2302,8 @@ mod tests {
// Allow rem name and class // Allow rem name and class
"name class", "name class",
// And the class allowed is NOT an account ... // And the class allowed is NOT an account ...
"group", EntryClass::Group.into(),
EntryClass::Group.into(),
); );
// Test allowed pres // Test allowed pres
@ -2287,6 +2389,7 @@ mod tests {
"name class", "name class",
// And the class allowed is account // And the class allowed is account
EntryClass::Account.into(), EntryClass::Account.into(),
EntryClass::Account.into(),
); );
test_acp_modify!(&me_pres_ro, vec![acp_allow.clone()], &r_set, false); test_acp_modify!(&me_pres_ro, vec![acp_allow.clone()], &r_set, false);
@ -2614,7 +2717,8 @@ mod tests {
search: Access::Allow(btreeset![Attribute::Name]), search: Access::Allow(btreeset![Attribute::Name]),
modify_pres: Access::Allow(BTreeSet::new()), modify_pres: Access::Allow(BTreeSet::new()),
modify_rem: Access::Allow(BTreeSet::new()), modify_rem: Access::Allow(BTreeSet::new()),
modify_class: AccessClass::Allow(BTreeSet::new()), modify_pres_class: AccessClass::Allow(BTreeSet::new()),
modify_rem_class: AccessClass::Allow(BTreeSet::new()),
}] }]
) )
} }
@ -2647,6 +2751,7 @@ mod tests {
Attribute::Name.as_ref(), Attribute::Name.as_ref(),
Attribute::Name.as_ref(), Attribute::Name.as_ref(),
EntryClass::Object.into(), EntryClass::Object.into(),
EntryClass::Object.into(),
)], )],
&r_set, &r_set,
vec![AccessEffectivePermission { vec![AccessEffectivePermission {
@ -2656,7 +2761,8 @@ mod tests {
search: Access::Allow(BTreeSet::new()), search: Access::Allow(BTreeSet::new()),
modify_pres: Access::Allow(btreeset![Attribute::Name]), modify_pres: Access::Allow(btreeset![Attribute::Name]),
modify_rem: Access::Allow(btreeset![Attribute::Name]), modify_rem: Access::Allow(btreeset![Attribute::Name]),
modify_class: AccessClass::Allow(btreeset![EntryClass::Object.into()]), modify_pres_class: AccessClass::Allow(btreeset![EntryClass::Object.into()]),
modify_rem_class: AccessClass::Allow(btreeset![EntryClass::Object.into()]),
}] }]
) )
} }
@ -2796,6 +2902,7 @@ mod tests {
&format!("{} {}", Attribute::UserAuthTokenSession, Attribute::Name), &format!("{} {}", Attribute::UserAuthTokenSession, Attribute::Name),
// And the class allowed is account, we don't use it though. // And the class allowed is account, we don't use it though.
EntryClass::Account.into(), EntryClass::Account.into(),
EntryClass::Account.into(),
); );
// NOTE! Syntax doesn't matter here, we just need to assert if the attr exists // NOTE! Syntax doesn't matter here, we just need to assert if the attr exists
@ -3296,6 +3403,7 @@ mod tests {
"name class", "name class",
// And the class allowed is account // And the class allowed is account
EntryClass::Account.into(), EntryClass::Account.into(),
EntryClass::Account.into(),
); );
// Test allowed pres // Test allowed pres
@ -3424,4 +3532,185 @@ mod tests {
// Finally test it! // Finally test it!
test_acp_search_reduce!(&se_anon_ro, vec![acp], r_set, ex_anon_some); test_acp_search_reduce!(&se_anon_ro, vec![acp], r_set, ex_anon_some);
} }
#[test]
fn test_access_protected_deny_create() {
sketching::test_init();
let ev1 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
);
let r1_set = vec![ev1];
let ev2 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
);
let r2_set = vec![ev2];
let ce_admin = CreateEvent::new_impersonate_identity(
Identity::from_impersonate_entry_readwrite(E_TEST_ACCOUNT_1.clone()),
vec![],
);
let acp = AccessControlCreate::from_raw(
"test_create",
Uuid::new_v4(),
// Apply to admin
UUID_TEST_GROUP_1,
// To create matching filter testperson
// Can this be empty?
filter_valid!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
// classes
EntryClass::Account.into(),
// attrs
"class name uuid",
);
// Test allowed to create
test_acp_create!(&ce_admin, vec![acp.clone()], &r1_set, true);
// Test reject create (not allowed attr)
test_acp_create!(&ce_admin, vec![acp.clone()], &r2_set, false);
}
#[test]
fn test_access_protected_deny_delete() {
sketching::test_init();
let ev1 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
)
.into_sealed_committed();
let r1_set = vec![Arc::new(ev1)];
let ev2 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
)
.into_sealed_committed();
let r2_set = vec![Arc::new(ev2)];
let de = DeleteEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
);
let acp = AccessControlDelete::from_raw(
"test_delete",
Uuid::new_v4(),
// Apply to admin
UUID_TEST_GROUP_1,
// To delete testperson
filter_valid!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
);
// Test allowed to delete
test_acp_delete!(&de, vec![acp.clone()], &r1_set, true);
// Test not allowed to delete
test_acp_delete!(&de, vec![acp.clone()], &r2_set, false);
}
#[test]
fn test_access_protected_deny_modify() {
sketching::test_init();
let ev1 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
)
.into_sealed_committed();
let r1_set = vec![Arc::new(ev1)];
let ev2 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
)
.into_sealed_committed();
let r2_set = vec![Arc::new(ev2)];
// Allow name and class, class is account
let acp_allow = AccessControlModify::from_raw(
"test_modify_allow",
Uuid::new_v4(),
// Apply to admin
UUID_TEST_GROUP_1,
// To modify testperson
filter_valid!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
// Allow pres disp name and class
"displayname class",
// Allow rem disp name and class
"displayname class",
// And the classes allowed to add/rem are as such
"system recycled",
"system recycled",
);
let me_pres = ModifyEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
modlist!([m_pres(Attribute::DisplayName, &Value::new_utf8s("value"))]),
);
// Test allowed pres
test_acp_modify!(&me_pres, vec![acp_allow.clone()], &r1_set, true);
// Test not allowed pres (due to system class)
test_acp_modify!(&me_pres, vec![acp_allow.clone()], &r2_set, false);
// Test that we can not remove class::system
let me_rem_sys = ModifyEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Class,
PartialValue::new_iname("testperson1")
)),
modlist!([m_remove(
Attribute::Class,
&EntryClass::System.to_partialvalue()
)]),
);
test_acp_modify!(&me_rem_sys, vec![acp_allow.clone()], &r2_set, false);
// Ensure that we can't add recycled.
let me_pres = ModifyEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
modlist!([m_pres(Attribute::Class, &EntryClass::Recycled.to_value())]),
);
test_acp_modify!(&me_pres, vec![acp_allow.clone()], &r1_set, false);
}
} }

View file

@ -1,21 +1,25 @@
use crate::prelude::*;
use hashbrown::HashMap;
use std::collections::BTreeSet;
use super::profiles::{ use super::profiles::{
AccessControlModify, AccessControlModifyResolved, AccessControlReceiverCondition, AccessControlModify, AccessControlModifyResolved, AccessControlReceiverCondition,
AccessControlTargetCondition, AccessControlTargetCondition,
}; };
use super::{AccessResult, AccessResultClass}; use super::protected::{
LOCKED_ENTRY_CLASSES, PROTECTED_MOD_ENTRY_CLASSES, PROTECTED_MOD_PRES_ENTRY_CLASSES,
PROTECTED_MOD_REM_ENTRY_CLASSES,
};
use super::{AccessBasicResult, AccessModResult};
use crate::prelude::*;
use hashbrown::HashMap;
use std::collections::BTreeSet;
use std::sync::Arc; use std::sync::Arc;
pub(super) enum ModifyResult<'a> { pub(super) enum ModifyResult<'a> {
Denied, Deny,
Grant, Grant,
Allow { Allow {
pres: BTreeSet<Attribute>, pres: BTreeSet<Attribute>,
rem: BTreeSet<Attribute>, rem: BTreeSet<Attribute>,
cls: BTreeSet<&'a str>, pres_cls: BTreeSet<&'a str>,
rem_cls: BTreeSet<&'a str>,
}, },
} }
@ -27,12 +31,17 @@ pub(super) fn apply_modify_access<'a>(
) -> ModifyResult<'a> { ) -> ModifyResult<'a> {
let mut denied = false; let mut denied = false;
let mut grant = false; let mut grant = false;
let mut constrain_pres = BTreeSet::default(); let mut constrain_pres = BTreeSet::default();
let mut allow_pres = BTreeSet::default(); let mut allow_pres = BTreeSet::default();
let mut constrain_rem = BTreeSet::default(); let mut constrain_rem = BTreeSet::default();
let mut allow_rem = BTreeSet::default(); let mut allow_rem = BTreeSet::default();
let mut constrain_cls = BTreeSet::default();
let mut allow_cls = BTreeSet::default(); let mut constrain_pres_cls = BTreeSet::default();
let mut allow_pres_cls = BTreeSet::default();
let mut constrain_rem_cls = BTreeSet::default();
let mut allow_rem_cls = BTreeSet::default();
// Some useful references. // Some useful references.
// - needed for checking entry manager conditions. // - needed for checking entry manager conditions.
@ -43,28 +52,53 @@ pub(super) fn apply_modify_access<'a>(
// kind of being three operations all in one. // kind of being three operations all in one.
match modify_ident_test(ident) { match modify_ident_test(ident) {
AccessResult::Denied => denied = true, AccessBasicResult::Deny => denied = true,
AccessResult::Grant => grant = true, AccessBasicResult::Grant => grant = true,
AccessResult::Ignore => {} AccessBasicResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain_pres.append(&mut set), }
AccessResult::Allow(mut set) => allow_pres.append(&mut set),
// Check with protected if we should proceed.
match modify_protected_attrs(ident, entry) {
AccessModResult::Deny => denied = true,
AccessModResult::Constrain {
mut pres_attr,
mut rem_attr,
pres_cls,
rem_cls,
} => {
constrain_rem.append(&mut rem_attr);
constrain_pres.append(&mut pres_attr);
if let Some(mut pres_cls) = pres_cls {
constrain_pres_cls.append(&mut pres_cls);
}
if let Some(mut rem_cls) = rem_cls {
constrain_rem_cls.append(&mut rem_cls);
}
}
// Can't grant.
// AccessModResult::Grant |
// Can't allow
AccessModResult::Allow { .. } | AccessModResult::Ignore => {}
} }
if !grant && !denied { if !grant && !denied {
// Check with protected if we should proceed.
// If it's a sync entry, constrain it. // If it's a sync entry, constrain it.
match modify_sync_constrain(ident, entry, sync_agreements) { match modify_sync_constrain(ident, entry, sync_agreements) {
AccessResult::Denied => denied = true, AccessModResult::Deny => denied = true,
AccessResult::Constrain(mut set) => { AccessModResult::Constrain {
constrain_rem.extend(set.iter().cloned()); mut pres_attr,
constrain_pres.append(&mut set) mut rem_attr,
..
} => {
constrain_rem.append(&mut rem_attr);
constrain_pres.append(&mut pres_attr);
} }
// Can't grant. // Can't grant.
AccessResult::Grant | // AccessModResult::Grant |
// Can't allow // Can't allow
AccessResult::Allow(_) | AccessModResult::Allow { .. } | AccessModResult::Ignore => {}
AccessResult::Ignore => {}
} }
// Setup the acp's here // Setup the acp's here
@ -122,35 +156,27 @@ pub(super) fn apply_modify_access<'a>(
.collect(); .collect();
match modify_pres_test(scoped_acp.as_slice()) { match modify_pres_test(scoped_acp.as_slice()) {
AccessResult::Denied => denied = true, AccessModResult::Deny => denied = true,
// Can never return a unilateral grant. // Can never return a unilateral grant.
AccessResult::Grant => {} // AccessModResult::Grant => {}
AccessResult::Ignore => {} AccessModResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain_pres.append(&mut set), AccessModResult::Constrain { .. } => {}
AccessResult::Allow(mut set) => allow_pres.append(&mut set), AccessModResult::Allow {
} mut pres_attr,
mut rem_attr,
match modify_rem_test(scoped_acp.as_slice()) { mut pres_class,
AccessResult::Denied => denied = true, mut rem_class,
// Can never return a unilateral grant. } => {
AccessResult::Grant => {} allow_pres.append(&mut pres_attr);
AccessResult::Ignore => {} allow_rem.append(&mut rem_attr);
AccessResult::Constrain(mut set) => constrain_rem.append(&mut set), allow_pres_cls.append(&mut pres_class);
AccessResult::Allow(mut set) => allow_rem.append(&mut set), allow_rem_cls.append(&mut rem_class);
} }
match modify_cls_test(scoped_acp.as_slice()) {
AccessResultClass::Denied => denied = true,
// Can never return a unilateral grant.
AccessResultClass::Grant => {}
AccessResultClass::Ignore => {}
AccessResultClass::Constrain(mut set) => constrain_cls.append(&mut set),
AccessResultClass::Allow(mut set) => allow_cls.append(&mut set),
} }
} }
if denied { if denied {
ModifyResult::Denied ModifyResult::Deny
} else if grant { } else if grant {
ModifyResult::Grant ModifyResult::Grant
} else { } else {
@ -168,31 +194,48 @@ pub(super) fn apply_modify_access<'a>(
allow_rem allow_rem
}; };
let allowed_cls = if !constrain_cls.is_empty() { let mut allowed_pres_cls = if !constrain_pres_cls.is_empty() {
// bit_and // bit_and
&constrain_cls & &allow_cls &constrain_pres_cls & &allow_pres_cls
} else { } else {
allow_cls allow_pres_cls
}; };
let mut allowed_rem_cls = if !constrain_rem_cls.is_empty() {
// bit_and
&constrain_rem_cls & &allow_rem_cls
} else {
allow_rem_cls
};
// Deny these classes from being part of any addition or removal to an entry
for protected_cls in PROTECTED_MOD_PRES_ENTRY_CLASSES.iter() {
allowed_pres_cls.remove(protected_cls.as_str());
}
for protected_cls in PROTECTED_MOD_REM_ENTRY_CLASSES.iter() {
allowed_rem_cls.remove(protected_cls.as_str());
}
ModifyResult::Allow { ModifyResult::Allow {
pres: allowed_pres, pres: allowed_pres,
rem: allowed_rem, rem: allowed_rem,
cls: allowed_cls, pres_cls: allowed_pres_cls,
rem_cls: allowed_rem_cls,
} }
} }
} }
fn modify_ident_test(ident: &Identity) -> AccessResult { fn modify_ident_test(ident: &Identity) -> AccessBasicResult {
match &ident.origin { match &ident.origin {
IdentType::Internal => { IdentType::Internal => {
trace!("Internal operation, bypassing access check"); trace!("Internal operation, bypassing access check");
// No need to check ACS // No need to check ACS
return AccessResult::Grant; return AccessBasicResult::Grant;
} }
IdentType::Synch(_) => { IdentType::Synch(_) => {
security_critical!("Blocking sync check"); security_critical!("Blocking sync check");
return AccessResult::Denied; return AccessBasicResult::Deny;
} }
IdentType::User(_) => {} IdentType::User(_) => {}
}; };
@ -201,53 +244,56 @@ fn modify_ident_test(ident: &Identity) -> AccessResult {
match ident.access_scope() { match ident.access_scope() {
AccessScope::ReadOnly | AccessScope::Synchronise => { AccessScope::ReadOnly | AccessScope::Synchronise => {
security_access!("denied ❌ - identity access scope is not permitted to modify"); security_access!("denied ❌ - identity access scope is not permitted to modify");
return AccessResult::Denied; return AccessBasicResult::Deny;
} }
AccessScope::ReadWrite => { AccessScope::ReadWrite => {
// As you were // As you were
} }
}; };
AccessResult::Ignore AccessBasicResult::Ignore
} }
fn modify_pres_test(scoped_acp: &[&AccessControlModify]) -> AccessResult { fn modify_pres_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessModResult<'a> {
let allowed_pres: BTreeSet<Attribute> = scoped_acp let pres_attr: BTreeSet<Attribute> = scoped_acp
.iter() .iter()
.flat_map(|acp| acp.presattrs.iter().cloned()) .flat_map(|acp| acp.presattrs.iter().cloned())
.collect(); .collect();
AccessResult::Allow(allowed_pres)
}
fn modify_rem_test(scoped_acp: &[&AccessControlModify]) -> AccessResult { let rem_attr: BTreeSet<Attribute> = scoped_acp
let allowed_rem: BTreeSet<Attribute> = scoped_acp
.iter() .iter()
.flat_map(|acp| acp.remattrs.iter().cloned()) .flat_map(|acp| acp.remattrs.iter().cloned())
.collect(); .collect();
AccessResult::Allow(allowed_rem)
}
// TODO: Should this be reverted to the Str borrow method? Or do we try to change let pres_class: BTreeSet<&'a str> = scoped_acp
// to EntryClass?
fn modify_cls_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessResultClass<'a> {
let allowed_classes: BTreeSet<&'a str> = scoped_acp
.iter() .iter()
.flat_map(|acp| acp.classes.iter().map(|s| s.as_str())) .flat_map(|acp| acp.pres_classes.iter().map(|s| s.as_str()))
.collect(); .collect();
AccessResultClass::Allow(allowed_classes)
let rem_class: BTreeSet<&'a str> = scoped_acp
.iter()
.flat_map(|acp| acp.rem_classes.iter().map(|s| s.as_str()))
.collect();
AccessModResult::Allow {
pres_attr,
rem_attr,
pres_class,
rem_class,
}
} }
fn modify_sync_constrain( fn modify_sync_constrain<'a>(
ident: &Identity, ident: &Identity,
entry: &Arc<EntrySealedCommitted>, entry: &Arc<EntrySealedCommitted>,
sync_agreements: &HashMap<Uuid, BTreeSet<Attribute>>, sync_agreements: &HashMap<Uuid, BTreeSet<Attribute>>,
) -> AccessResult { ) -> AccessModResult<'a> {
match &ident.origin { match &ident.origin {
IdentType::Internal => AccessResult::Ignore, IdentType::Internal => AccessModResult::Ignore,
IdentType::Synch(_) => { IdentType::Synch(_) => {
// Allowed to mod sync objects. Later we'll probably need to check the limits of what // Allowed to mod sync objects. Later we'll probably need to check the limits of what
// it can do if we go that way. // it can do if we go that way.
AccessResult::Ignore AccessModResult::Ignore
} }
IdentType::User(_) => { IdentType::User(_) => {
// We need to meet these conditions. // We need to meet these conditions.
@ -259,7 +305,7 @@ fn modify_sync_constrain(
.unwrap_or(false); .unwrap_or(false);
if !is_sync { if !is_sync {
return AccessResult::Ignore; return AccessModResult::Ignore;
} }
if let Some(sync_uuid) = entry.get_ava_single_refer(Attribute::SyncParentUuid) { if let Some(sync_uuid) = entry.get_ava_single_refer(Attribute::SyncParentUuid) {
@ -274,11 +320,115 @@ fn modify_sync_constrain(
set.extend(sync_yield_authority.iter().cloned()) set.extend(sync_yield_authority.iter().cloned())
} }
AccessResult::Constrain(set) AccessModResult::Constrain {
pres_attr: set.clone(),
rem_attr: set,
pres_cls: None,
rem_cls: None,
}
} else { } else {
warn!(entry = ?entry.get_uuid(), "sync_parent_uuid not found on sync object, preventing all access"); warn!(entry = ?entry.get_uuid(), "sync_parent_uuid not found on sync object, preventing all access");
AccessResult::Denied AccessModResult::Deny
} }
} }
} }
} }
/// Verify if the modification runs into limits that are defined by our protection rules.
fn modify_protected_attrs<'a>(
ident: &Identity,
entry: &Arc<EntrySealedCommitted>,
) -> AccessModResult<'a> {
match &ident.origin {
IdentType::Internal | IdentType::Synch(_) => {
// We don't constraint or influence these.
AccessModResult::Ignore
}
IdentType::User(_) => {
if let Some(classes) = entry.get_ava_as_iutf8(Attribute::Class) {
if classes.is_disjoint(&PROTECTED_MOD_ENTRY_CLASSES) {
// Not protected, go ahead
AccessModResult::Ignore
} else {
// Okay, the entry is protected, apply the full ruleset.
modify_protected_entry_attrs(classes)
}
} else {
// Nothing to check - this entry will fail to modify anyway because it has
// no classes
AccessModResult::Ignore
}
}
}
}
fn modify_protected_entry_attrs<'a>(classes: &BTreeSet<String>) -> AccessModResult<'a> {
// This is where the majority of the logic is - this contains the modification
// rules as they apply.
// First check for the hard-deny rules.
if !classes.is_disjoint(&LOCKED_ENTRY_CLASSES) {
// Hard deny attribute modifications to these types.
return AccessModResult::Deny;
}
let mut constrain_attrs = BTreeSet::default();
// Allows removal of the recycled class specifically on recycled entries.
if classes.contains(EntryClass::Recycled.into()) {
constrain_attrs.extend([Attribute::Class]);
}
if classes.contains(EntryClass::ClassType.into()) {
constrain_attrs.extend([Attribute::May, Attribute::Must]);
}
if classes.contains(EntryClass::SystemConfig.into()) {
constrain_attrs.extend([Attribute::BadlistPassword]);
}
// Allow domain settings.
if classes.contains(EntryClass::DomainInfo.into()) {
constrain_attrs.extend([
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::LdapAllowUnixPwBind,
Attribute::FernetPrivateKeyStr,
Attribute::Es256PrivateKeyDer,
Attribute::KeyActionRevoke,
Attribute::KeyActionRotate,
Attribute::IdVerificationEcKey,
Attribute::DeniedName,
Attribute::DomainDisplayName,
Attribute::Image,
]);
}
// Allow account policy related attributes to be changed on dyngroup
if classes.contains(EntryClass::DynGroup.into()) {
constrain_attrs.extend([
Attribute::AuthSessionExpiry,
Attribute::AuthPasswordMinimumLength,
Attribute::CredentialTypeMinimum,
Attribute::PrivilegeExpiry,
Attribute::WebauthnAttestationCaList,
Attribute::LimitSearchMaxResults,
Attribute::LimitSearchMaxFilterTest,
Attribute::AllowPrimaryCredFallback,
]);
}
// If we don't constrain the attributes at all, we have to deny the change
// from proceeding.
if constrain_attrs.is_empty() {
AccessModResult::Deny
} else {
AccessModResult::Constrain {
pres_attr: constrain_attrs.clone(),
rem_attr: constrain_attrs,
pres_cls: None,
rem_cls: None,
}
}
}

View file

@ -266,9 +266,10 @@ pub struct AccessControlModifyResolved<'a> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AccessControlModify { pub struct AccessControlModify {
pub acp: AccessControlProfile, pub acp: AccessControlProfile,
pub classes: Vec<AttrString>,
pub presattrs: Vec<Attribute>, pub presattrs: Vec<Attribute>,
pub remattrs: Vec<Attribute>, pub remattrs: Vec<Attribute>,
pub pres_classes: Vec<AttrString>,
pub rem_classes: Vec<AttrString>,
} }
impl AccessControlModify { impl AccessControlModify {
@ -293,14 +294,25 @@ impl AccessControlModify {
.map(|i| i.map(Attribute::from).collect()) .map(|i| i.map(Attribute::from).collect())
.unwrap_or_default(); .unwrap_or_default();
let classes = value let classes: Vec<AttrString> = value
.get_ava_iter_iutf8(Attribute::AcpModifyClass) .get_ava_iter_iutf8(Attribute::AcpModifyClass)
.map(|i| i.map(AttrString::from).collect()) .map(|i| i.map(AttrString::from).collect())
.unwrap_or_default(); .unwrap_or_default();
let pres_classes = value
.get_ava_iter_iutf8(Attribute::AcpModifyPresentClass)
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_else(|| classes.clone());
let rem_classes = value
.get_ava_iter_iutf8(Attribute::AcpModifyRemoveClass)
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_else(|| classes);
Ok(AccessControlModify { Ok(AccessControlModify {
acp: AccessControlProfile::try_from(qs, value)?, acp: AccessControlProfile::try_from(qs, value)?,
classes, pres_classes,
rem_classes,
presattrs, presattrs,
remattrs, remattrs,
}) })
@ -316,7 +328,8 @@ impl AccessControlModify {
targetscope: Filter<FilterValid>, targetscope: Filter<FilterValid>,
presattrs: &str, presattrs: &str,
remattrs: &str, remattrs: &str,
classes: &str, pres_classes: &str,
rem_classes: &str,
) -> Self { ) -> Self {
AccessControlModify { AccessControlModify {
acp: AccessControlProfile { acp: AccessControlProfile {
@ -325,7 +338,14 @@ impl AccessControlModify {
receiver: AccessControlReceiver::Group(btreeset!(receiver)), receiver: AccessControlReceiver::Group(btreeset!(receiver)),
target: AccessControlTarget::Scope(targetscope), target: AccessControlTarget::Scope(targetscope),
}, },
classes: classes.split_whitespace().map(AttrString::from).collect(), pres_classes: pres_classes
.split_whitespace()
.map(AttrString::from)
.collect(),
rem_classes: rem_classes
.split_whitespace()
.map(AttrString::from)
.collect(),
presattrs: presattrs.split_whitespace().map(Attribute::from).collect(), presattrs: presattrs.split_whitespace().map(Attribute::from).collect(),
remattrs: remattrs.split_whitespace().map(Attribute::from).collect(), remattrs: remattrs.split_whitespace().map(Attribute::from).collect(),
} }
@ -340,7 +360,8 @@ impl AccessControlModify {
target: AccessControlTarget, target: AccessControlTarget,
presattrs: &str, presattrs: &str,
remattrs: &str, remattrs: &str,
classes: &str, pres_classes: &str,
rem_classes: &str,
) -> Self { ) -> Self {
AccessControlModify { AccessControlModify {
acp: AccessControlProfile { acp: AccessControlProfile {
@ -349,7 +370,14 @@ impl AccessControlModify {
receiver: AccessControlReceiver::EntryManager, receiver: AccessControlReceiver::EntryManager,
target, target,
}, },
classes: classes.split_whitespace().map(AttrString::from).collect(), pres_classes: pres_classes
.split_whitespace()
.map(AttrString::from)
.collect(),
rem_classes: rem_classes
.split_whitespace()
.map(AttrString::from)
.collect(),
presattrs: presattrs.split_whitespace().map(Attribute::from).collect(), presattrs: presattrs.split_whitespace().map(Attribute::from).collect(),
remattrs: remattrs.split_whitespace().map(Attribute::from).collect(), remattrs: remattrs.split_whitespace().map(Attribute::from).collect(),
} }

View file

@ -0,0 +1,83 @@
use crate::prelude::EntryClass;
use std::collections::BTreeSet;
use std::sync::LazyLock;
/// These entry classes may not be created or deleted, and may invoke some protection rules
/// if on an entry.
pub static PROTECTED_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
EntryClass::SyncObject,
EntryClass::Tombstone,
EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});
/// Entries with these classes are protected from modifications - not that
/// sync object is not present here as there are separate rules for that in
/// the modification access module.
///
/// Recycled is also not protected here as it needs to be able to be removed
/// by a recycle bin admin.
pub static PROTECTED_MOD_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
// EntryClass::SyncObject,
EntryClass::Tombstone,
EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});
/// These classes may NOT be added to ANY ENTRY
pub static PROTECTED_MOD_PRES_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
EntryClass::SyncObject,
EntryClass::Tombstone,
EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});
/// These classes may NOT be removed from ANY ENTRY
pub static PROTECTED_MOD_REM_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
EntryClass::SyncObject,
EntryClass::Tombstone,
// EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});
/// Entries with these classes may not be modified under any circumstance.
pub static LOCKED_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::Tombstone,
// EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});

View file

@ -4,11 +4,11 @@ use std::collections::BTreeSet;
use super::profiles::{ use super::profiles::{
AccessControlReceiverCondition, AccessControlSearchResolved, AccessControlTargetCondition, AccessControlReceiverCondition, AccessControlSearchResolved, AccessControlTargetCondition,
}; };
use super::AccessResult; use super::AccessSrchResult;
use std::sync::Arc; use std::sync::Arc;
pub(super) enum SearchResult { pub(super) enum SearchResult {
Denied, Deny,
Grant, Grant,
Allow(BTreeSet<Attribute>), Allow(BTreeSet<Attribute>),
} }
@ -23,32 +23,32 @@ pub(super) fn apply_search_access(
// that. // that.
let mut denied = false; let mut denied = false;
let mut grant = false; let mut grant = false;
let mut constrain = BTreeSet::default(); let constrain = BTreeSet::default();
let mut allow = BTreeSet::default(); let mut allow = BTreeSet::default();
// The access control profile // The access control profile
match search_filter_entry(ident, related_acp, entry) { match search_filter_entry(ident, related_acp, entry) {
AccessResult::Denied => denied = true, AccessSrchResult::Deny => denied = true,
AccessResult::Grant => grant = true, AccessSrchResult::Grant => grant = true,
AccessResult::Ignore => {} AccessSrchResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain.append(&mut set), // AccessSrchResult::Constrain { mut attr } => constrain.append(&mut attr),
AccessResult::Allow(mut set) => allow.append(&mut set), AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
}; };
match search_oauth2_filter_entry(ident, entry) { match search_oauth2_filter_entry(ident, entry) {
AccessResult::Denied => denied = true, AccessSrchResult::Deny => denied = true,
AccessResult::Grant => grant = true, AccessSrchResult::Grant => grant = true,
AccessResult::Ignore => {} AccessSrchResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain.append(&mut set), // AccessSrchResult::Constrain { mut attr } => constrain.append(&mut attr),
AccessResult::Allow(mut set) => allow.append(&mut set), AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
}; };
match search_sync_account_filter_entry(ident, entry) { match search_sync_account_filter_entry(ident, entry) {
AccessResult::Denied => denied = true, AccessSrchResult::Deny => denied = true,
AccessResult::Grant => grant = true, AccessSrchResult::Grant => grant = true,
AccessResult::Ignore => {} AccessSrchResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain.append(&mut set), // AccessSrchResult::Constrain{ mut attr } => constrain.append(&mut attr),
AccessResult::Allow(mut set) => allow.append(&mut set), AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
}; };
// We'll add more modules later. // We'll add more modules later.
@ -56,7 +56,7 @@ pub(super) fn apply_search_access(
// Now finalise the decision. // Now finalise the decision.
if denied { if denied {
SearchResult::Denied SearchResult::Deny
} else if grant { } else if grant {
SearchResult::Grant SearchResult::Grant
} else { } else {
@ -74,17 +74,17 @@ fn search_filter_entry(
ident: &Identity, ident: &Identity,
related_acp: &[AccessControlSearchResolved], related_acp: &[AccessControlSearchResolved],
entry: &Arc<EntrySealedCommitted>, entry: &Arc<EntrySealedCommitted>,
) -> AccessResult { ) -> AccessSrchResult {
// If this is an internal search, return our working set. // If this is an internal search, return our working set.
match &ident.origin { match &ident.origin {
IdentType::Internal => { IdentType::Internal => {
trace!(uuid = ?entry.get_display_id(), "Internal operation, bypassing access check"); trace!(uuid = ?entry.get_display_id(), "Internal operation, bypassing access check");
// No need to check ACS // No need to check ACS
return AccessResult::Grant; return AccessSrchResult::Grant;
} }
IdentType::Synch(_) => { IdentType::Synch(_) => {
security_debug!(uuid = ?entry.get_display_id(), "Blocking sync check"); security_debug!(uuid = ?entry.get_display_id(), "Blocking sync check");
return AccessResult::Denied; return AccessSrchResult::Deny;
} }
IdentType::User(_) => {} IdentType::User(_) => {}
}; };
@ -95,7 +95,7 @@ fn search_filter_entry(
security_debug!( security_debug!(
"denied ❌ - identity access scope 'Synchronise' is not permitted to search" "denied ❌ - identity access scope 'Synchronise' is not permitted to search"
); );
return AccessResult::Denied; return AccessSrchResult::Deny;
} }
AccessScope::ReadOnly | AccessScope::ReadWrite => { AccessScope::ReadOnly | AccessScope::ReadWrite => {
// As you were // As you were
@ -161,16 +161,21 @@ fn search_filter_entry(
.flatten() .flatten()
.collect(); .collect();
AccessResult::Allow(allowed_attrs) AccessSrchResult::Allow {
attr: allowed_attrs,
}
} }
fn search_oauth2_filter_entry(ident: &Identity, entry: &Arc<EntrySealedCommitted>) -> AccessResult { fn search_oauth2_filter_entry(
ident: &Identity,
entry: &Arc<EntrySealedCommitted>,
) -> AccessSrchResult {
match &ident.origin { match &ident.origin {
IdentType::Internal | IdentType::Synch(_) => AccessResult::Ignore, IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
IdentType::User(iuser) => { IdentType::User(iuser) => {
if iuser.entry.get_uuid() == UUID_ANONYMOUS { if iuser.entry.get_uuid() == UUID_ANONYMOUS {
debug!("Anonymous can't access OAuth2 entries, ignoring"); debug!("Anonymous can't access OAuth2 entries, ignoring");
return AccessResult::Ignore; return AccessSrchResult::Ignore;
} }
let contains_o2_rs = entry let contains_o2_rs = entry
@ -190,16 +195,18 @@ fn search_oauth2_filter_entry(ident: &Identity, entry: &Arc<EntrySealedCommitted
if contains_o2_rs && contains_o2_scope_member { if contains_o2_rs && contains_o2_scope_member {
security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted an oauth2 scope by this entry"); security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted an oauth2 scope by this entry");
return AccessResult::Allow(btreeset!( return AccessSrchResult::Allow {
Attribute::Class, attr: btreeset!(
Attribute::DisplayName, Attribute::Class,
Attribute::Uuid, Attribute::DisplayName,
Attribute::Name, Attribute::Uuid,
Attribute::OAuth2RsOriginLanding, Attribute::Name,
Attribute::Image Attribute::OAuth2RsOriginLanding,
)); Attribute::Image
),
};
} }
AccessResult::Ignore AccessSrchResult::Ignore
} }
} }
} }
@ -207,9 +214,9 @@ fn search_oauth2_filter_entry(ident: &Identity, entry: &Arc<EntrySealedCommitted
fn search_sync_account_filter_entry( fn search_sync_account_filter_entry(
ident: &Identity, ident: &Identity,
entry: &Arc<EntrySealedCommitted>, entry: &Arc<EntrySealedCommitted>,
) -> AccessResult { ) -> AccessSrchResult {
match &ident.origin { match &ident.origin {
IdentType::Internal | IdentType::Synch(_) => AccessResult::Ignore, IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
IdentType::User(iuser) => { IdentType::User(iuser) => {
// Is the user a synced object? // Is the user a synced object?
let is_user_sync_account = iuser let is_user_sync_account = iuser
@ -244,16 +251,18 @@ fn search_sync_account_filter_entry(
// We finally got here! // We finally got here!
security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a synchronised account from this sync account"); security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a synchronised account from this sync account");
return AccessResult::Allow(btreeset!( return AccessSrchResult::Allow {
Attribute::Class, attr: btreeset!(
Attribute::Uuid, Attribute::Class,
Attribute::SyncCredentialPortal Attribute::Uuid,
)); Attribute::SyncCredentialPortal
),
};
} }
} }
} }
// Fall through // Fall through
AccessResult::Ignore AccessSrchResult::Ignore
} }
} }
} }