mirror of
https://github.com/kanidm/kanidm.git
synced 2025-04-12 21:35:39 +02:00
20250314 remove protected plugin (#3504)
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:
parent
ec3db91da0
commit
a2eae53328
49
Cargo.lock
generated
49
Cargo.lock
generated
|
@ -232,9 +232,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.21"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0cf008e5e1a9e9e22a7d3c9a4992e21a350290069e36d8fb72304ed17e8f2d2"
|
||||
checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-core",
|
||||
|
@ -1167,9 +1167,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
|
@ -2532,14 +2532,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.61"
|
||||
version = "0.1.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
|
||||
checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
@ -2594,9 +2595,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "icu_locid_transform_data"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
|
||||
checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
|
@ -2618,9 +2619,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
|
||||
checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
|
@ -2639,9 +2640,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
|
||||
checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
|
@ -3483,9 +3484,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.26"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
|
@ -3948,9 +3949,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.1"
|
||||
version = "1.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
|
||||
checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
|
@ -4483,9 +4484,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.10"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944"
|
||||
checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
|
@ -4947,9 +4948,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.0"
|
||||
version = "0.103.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f"
|
||||
checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
|
@ -5576,9 +5577,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.40"
|
||||
version = "0.3.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
|
@ -5599,9 +5600,9 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
|||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.21"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
|
|
|
@ -22,6 +22,8 @@ pub enum Attribute {
|
|||
AcpCreateClass,
|
||||
AcpEnable,
|
||||
AcpModifyClass,
|
||||
AcpModifyPresentClass,
|
||||
AcpModifyRemoveClass,
|
||||
AcpModifyPresentAttr,
|
||||
AcpModifyRemovedAttr,
|
||||
AcpReceiver,
|
||||
|
@ -255,6 +257,8 @@ impl Attribute {
|
|||
Attribute::AcpCreateClass => ATTR_ACP_CREATE_CLASS,
|
||||
Attribute::AcpEnable => ATTR_ACP_ENABLE,
|
||||
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::AcpModifyRemovedAttr => ATTR_ACP_MODIFY_REMOVEDATTR,
|
||||
Attribute::AcpReceiver => ATTR_ACP_RECEIVER,
|
||||
|
@ -440,6 +444,8 @@ impl Attribute {
|
|||
ATTR_ACP_CREATE_CLASS => Attribute::AcpCreateClass,
|
||||
ATTR_ACP_ENABLE => Attribute::AcpEnable,
|
||||
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_REMOVEDATTR => Attribute::AcpModifyRemovedAttr,
|
||||
ATTR_ACP_RECEIVER => Attribute::AcpReceiver,
|
||||
|
|
|
@ -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_ENABLE: &str = "acp_enable";
|
||||
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_REMOVEDATTR: &str = "acp_modify_removedattr";
|
||||
pub const ATTR_ACP_RECEIVER_GROUP: &str = "acp_receiver_group";
|
||||
|
|
|
@ -33,7 +33,7 @@ pub enum ScimAttributeEffectiveAccess {
|
|||
/// All attributes on the entry have this permission granted
|
||||
Grant,
|
||||
/// All attributes on the entry have this permission denied
|
||||
Denied,
|
||||
Deny,
|
||||
/// The following attributes on the entry have this permission granted
|
||||
Allow(BTreeSet<Attribute>),
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ impl ScimAttributeEffectiveAccess {
|
|||
pub fn check(&self, attr: &Attribute) -> bool {
|
||||
match self {
|
||||
Self::Grant => true,
|
||||
Self::Denied => false,
|
||||
Self::Deny => false,
|
||||
Self::Allow(set) => set.contains(attr),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -330,6 +330,10 @@ pub const UUID_SCHEMA_ATTR_DOMAIN_ALLOW_EASTER_EGGS: Uuid =
|
|||
pub const UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000187");
|
||||
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
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
|
|
@ -599,19 +599,19 @@ impl IdmServerProxyWriteTransaction<'_> {
|
|||
}
|
||||
|
||||
let eperm_search_primary_cred = match &eperm.search {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
|
||||
};
|
||||
|
||||
let eperm_mod_primary_cred = match &eperm.modify_pres {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
|
||||
};
|
||||
|
||||
let eperm_rem_primary_cred = match &eperm.modify_rem {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
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;
|
||||
|
||||
let eperm_search_passkeys = match &eperm.search {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
|
||||
};
|
||||
|
||||
let eperm_mod_passkeys = match &eperm.modify_pres {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
|
||||
};
|
||||
|
||||
let eperm_rem_passkeys = match &eperm.modify_rem {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
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 eperm_search_attested_passkeys = match &eperm.search {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
|
||||
};
|
||||
|
||||
let eperm_mod_attested_passkeys = match &eperm.modify_pres {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
|
||||
};
|
||||
|
||||
let eperm_rem_attested_passkeys = match &eperm.modify_rem {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
|
||||
};
|
||||
|
@ -662,19 +662,19 @@ impl IdmServerProxyWriteTransaction<'_> {
|
|||
&& eperm_rem_attested_passkeys;
|
||||
|
||||
let eperm_search_unixcred = match &eperm.search {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
|
||||
};
|
||||
|
||||
let eperm_mod_unixcred = match &eperm.modify_pres {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
|
||||
};
|
||||
|
||||
let eperm_rem_unixcred = match &eperm.modify_rem {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
|
||||
};
|
||||
|
@ -685,19 +685,19 @@ impl IdmServerProxyWriteTransaction<'_> {
|
|||
&& eperm_rem_unixcred;
|
||||
|
||||
let eperm_search_sshpubkey = match &eperm.search {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
|
||||
};
|
||||
|
||||
let eperm_mod_sshpubkey = match &eperm.modify_pres {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
|
||||
};
|
||||
|
||||
let eperm_rem_sshpubkey = match &eperm.modify_rem {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
|
||||
};
|
||||
|
@ -726,7 +726,7 @@ impl IdmServerProxyWriteTransaction<'_> {
|
|||
})?;
|
||||
|
||||
match &eperm.search {
|
||||
Access::Denied => false,
|
||||
Access::Deny => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains(&Attribute::SyncCredentialPortal),
|
||||
}
|
||||
|
|
|
@ -72,6 +72,8 @@ pub struct BuiltinAcp {
|
|||
modify_present_attrs: Vec<Attribute>,
|
||||
modify_removed_attrs: Vec<Attribute>,
|
||||
modify_classes: Vec<EntryClass>,
|
||||
modify_present_classes: Vec<EntryClass>,
|
||||
modify_remove_classes: Vec<EntryClass>,
|
||||
create_classes: Vec<EntryClass>,
|
||||
create_attrs: Vec<Attribute>,
|
||||
}
|
||||
|
@ -159,9 +161,19 @@ impl From<BuiltinAcp> for EntryInitNew {
|
|||
value.modify_removed_attrs.into_iter().for_each(|attr| {
|
||||
entry.add_ava(Attribute::AcpModifyRemovedAttr, Value::from(attr));
|
||||
});
|
||||
|
||||
value.modify_classes.into_iter().for_each(|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| {
|
||||
entry.add_ava(Attribute::AcpCreateClass, Value::from(class));
|
||||
});
|
||||
|
@ -214,7 +226,7 @@ lazy_static! {
|
|||
ATTR_RECYCLED.to_string()
|
||||
)),
|
||||
modify_removed_attrs: vec![Attribute::Class],
|
||||
modify_classes: vec![EntryClass::Recycled],
|
||||
modify_remove_classes: vec![EntryClass::Recycled],
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
@ -425,6 +437,7 @@ lazy_static! {
|
|||
EntryClass::AccessControlCreate,
|
||||
EntryClass::AccessControlDelete,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -695,7 +695,6 @@ mod tests {
|
|||
|
||||
let e = entry_init!(
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Class, EntryClass::System.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson")),
|
||||
(Attribute::DisplayName, Value::new_iname("testperson")),
|
||||
(
|
||||
|
@ -726,7 +725,6 @@ mod tests {
|
|||
|
||||
let e = entry_init!(
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Class, EntryClass::System.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson")),
|
||||
(Attribute::DisplayName, Value::new_iname("testperson")),
|
||||
(
|
||||
|
|
|
@ -22,7 +22,6 @@ mod jwskeygen;
|
|||
mod keyobject;
|
||||
mod memberof;
|
||||
mod namehistory;
|
||||
mod protected;
|
||||
mod refint;
|
||||
mod session;
|
||||
mod spn;
|
||||
|
@ -44,6 +43,7 @@ trait Plugin {
|
|||
Err(OperationError::InvalidState)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn pre_create(
|
||||
_qs: &mut QueryServerWriteTransaction,
|
||||
// List of what we will commit that is valid?
|
||||
|
@ -243,13 +243,13 @@ impl Plugins {
|
|||
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(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
cand: &[Entry<EntrySealed, EntryNew>],
|
||||
ce: &CreateEvent,
|
||||
_qs: &mut QueryServerWriteTransaction,
|
||||
_cand: &[Entry<EntrySealed, EntryNew>],
|
||||
_ce: &CreateEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
protected::Protected::pre_create(qs, cand, ce)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "plugins::run_post_create", skip_all)]
|
||||
|
@ -269,7 +269,6 @@ impl Plugins {
|
|||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
me: &ModifyEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
protected::Protected::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)?;
|
||||
cred_import::CredImport::pre_modify(qs, pre_cand, cand, me)?;
|
||||
|
@ -305,7 +304,6 @@ impl Plugins {
|
|||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
me: &BatchModifyEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
protected::Protected::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)?;
|
||||
cred_import::CredImport::pre_batch_modify(qs, pre_cand, cand, me)?;
|
||||
|
@ -340,7 +338,6 @@ impl Plugins {
|
|||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
de: &DeleteEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
protected::Protected::pre_delete(qs, cand, de)?;
|
||||
memberof::MemberOf::pre_delete(qs, cand, de)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()),
|
||||
|_| {}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1366,6 +1366,36 @@ impl SchemaWriteTransaction<'_> {
|
|||
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(
|
||||
Attribute::EntryManagedBy,
|
||||
SchemaAttribute {
|
||||
|
@ -2069,6 +2099,8 @@ impl SchemaWriteTransaction<'_> {
|
|||
Attribute::AcpModifyRemovedAttr,
|
||||
Attribute::AcpModifyPresentAttr,
|
||||
Attribute::AcpModifyClass,
|
||||
Attribute::AcpModifyPresentClass,
|
||||
Attribute::AcpModifyRemoveClass,
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
use super::profiles::{
|
||||
AccessControlCreateResolved, AccessControlReceiverCondition, AccessControlTargetCondition,
|
||||
};
|
||||
use super::protected::PROTECTED_ENTRY_CLASSES;
|
||||
use crate::prelude::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
pub(super) enum CreateResult {
|
||||
Denied,
|
||||
Deny,
|
||||
Grant,
|
||||
}
|
||||
|
||||
enum IResult {
|
||||
Denied,
|
||||
Deny,
|
||||
Grant,
|
||||
Ignore,
|
||||
}
|
||||
|
@ -25,25 +26,25 @@ pub(super) fn apply_create_access<'a>(
|
|||
|
||||
// This module can never yield a grant.
|
||||
match protected_filter_entry(ident, entry) {
|
||||
IResult::Denied => denied = true,
|
||||
IResult::Deny => denied = true,
|
||||
IResult::Grant | IResult::Ignore => {}
|
||||
}
|
||||
|
||||
match create_filter_entry(ident, related_acp, entry) {
|
||||
IResult::Denied => denied = true,
|
||||
IResult::Deny => denied = true,
|
||||
IResult::Grant => grant = true,
|
||||
IResult::Ignore => {}
|
||||
}
|
||||
|
||||
if denied {
|
||||
// Something explicitly said no.
|
||||
CreateResult::Denied
|
||||
CreateResult::Deny
|
||||
} else if grant {
|
||||
// Something said yes
|
||||
CreateResult::Grant
|
||||
} else {
|
||||
// Nothing said yes.
|
||||
CreateResult::Denied
|
||||
CreateResult::Deny
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +61,7 @@ fn create_filter_entry<'a>(
|
|||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_critical!("Blocking sync check");
|
||||
return IResult::Denied;
|
||||
return IResult::Deny;
|
||||
}
|
||||
IdentType::User(_) => {}
|
||||
};
|
||||
|
@ -69,7 +70,7 @@ fn create_filter_entry<'a>(
|
|||
match ident.access_scope() {
|
||||
AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to create");
|
||||
return IResult::Denied;
|
||||
return IResult::Deny;
|
||||
}
|
||||
AccessScope::ReadWrite => {
|
||||
// As you were
|
||||
|
@ -96,7 +97,7 @@ fn create_filter_entry<'a>(
|
|||
Some(s) => s.collect(),
|
||||
None => {
|
||||
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(_) => {
|
||||
security_access!("sync agreements may not directly create entities");
|
||||
IResult::Denied
|
||||
IResult::Deny
|
||||
}
|
||||
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 create with protected class type");
|
||||
IResult::Denied
|
||||
} else {
|
||||
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.
|
||||
// Nothing to check - this entry will fail to create anyway because it has
|
||||
// no classes
|
||||
IResult::Ignore
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
use super::profiles::{
|
||||
AccessControlDeleteResolved, AccessControlReceiverCondition, AccessControlTargetCondition,
|
||||
};
|
||||
use super::protected::PROTECTED_ENTRY_CLASSES;
|
||||
use crate::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) enum DeleteResult {
|
||||
Denied,
|
||||
Deny,
|
||||
Grant,
|
||||
}
|
||||
|
||||
enum IResult {
|
||||
Denied,
|
||||
Deny,
|
||||
Grant,
|
||||
Ignore,
|
||||
}
|
||||
|
@ -24,25 +25,25 @@ pub(super) fn apply_delete_access<'a>(
|
|||
let mut grant = false;
|
||||
|
||||
match protected_filter_entry(ident, entry) {
|
||||
IResult::Denied => denied = true,
|
||||
IResult::Deny => denied = true,
|
||||
IResult::Grant | IResult::Ignore => {}
|
||||
}
|
||||
|
||||
match delete_filter_entry(ident, related_acp, entry) {
|
||||
IResult::Denied => denied = true,
|
||||
IResult::Deny => denied = true,
|
||||
IResult::Grant => grant = true,
|
||||
IResult::Ignore => {}
|
||||
}
|
||||
|
||||
if denied {
|
||||
// Something explicitly said no.
|
||||
DeleteResult::Denied
|
||||
DeleteResult::Deny
|
||||
} else if grant {
|
||||
// Something said yes
|
||||
DeleteResult::Grant
|
||||
} else {
|
||||
// Nothing said yes.
|
||||
DeleteResult::Denied
|
||||
DeleteResult::Deny
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,7 +60,7 @@ fn delete_filter_entry<'a>(
|
|||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_critical!("Blocking sync check");
|
||||
return IResult::Denied;
|
||||
return IResult::Deny;
|
||||
}
|
||||
IdentType::User(_) => {}
|
||||
};
|
||||
|
@ -68,7 +69,7 @@ fn delete_filter_entry<'a>(
|
|||
match ident.access_scope() {
|
||||
AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to delete");
|
||||
return IResult::Denied;
|
||||
return IResult::Deny;
|
||||
}
|
||||
AccessScope::ReadWrite => {
|
||||
// As you were
|
||||
|
@ -152,28 +153,30 @@ fn protected_filter_entry(ident: &Identity, entry: &Arc<EntrySealedCommitted>) -
|
|||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_access!("sync agreements may not directly delete entities");
|
||||
IResult::Denied
|
||||
IResult::Deny
|
||||
}
|
||||
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.
|
||||
if entry.get_uuid() <= UUID_ANONYMOUS {
|
||||
security_access!("attempt to delete system builtin entry");
|
||||
return IResult::Denied;
|
||||
return IResult::Deny;
|
||||
}
|
||||
|
||||
// Checks exhausted, no more input from us
|
||||
IResult::Ignore
|
||||
// Prevent deleting some protected types.
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,12 +50,13 @@ mod create;
|
|||
mod delete;
|
||||
mod modify;
|
||||
pub mod profiles;
|
||||
mod protected;
|
||||
mod search;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Access {
|
||||
Grant,
|
||||
Denied,
|
||||
Deny,
|
||||
Allow(BTreeSet<Attribute>),
|
||||
}
|
||||
|
||||
|
@ -63,7 +64,7 @@ impl From<&Access> for ScimAttributeEffectiveAccess {
|
|||
fn from(value: &Access) -> Self {
|
||||
match value {
|
||||
Access::Grant => Self::Grant,
|
||||
Access::Denied => Self::Denied,
|
||||
Access::Deny => Self::Deny,
|
||||
Access::Allow(set) => Self::Allow(set.clone()),
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +73,7 @@ impl From<&Access> for ScimAttributeEffectiveAccess {
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AccessClass {
|
||||
Grant,
|
||||
Denied,
|
||||
Deny,
|
||||
Allow(BTreeSet<AttrString>),
|
||||
}
|
||||
|
||||
|
@ -86,12 +87,22 @@ pub struct AccessEffectivePermission {
|
|||
pub search: Access,
|
||||
pub modify_pres: 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.
|
||||
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.
|
||||
Grant,
|
||||
// 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
|
||||
// allow anything, it constrains what might be allowed
|
||||
// by a later module.
|
||||
Constrain(BTreeSet<Attribute>),
|
||||
// Allow these attributes within constraints.
|
||||
Allow(BTreeSet<Attribute>),
|
||||
/*
|
||||
Constrain {
|
||||
attr: BTreeSet<Attribute>,
|
||||
},
|
||||
*/
|
||||
Allow { attr: BTreeSet<Attribute> },
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum AccessResultClass<'a> {
|
||||
pub enum AccessModResult<'a> {
|
||||
// Deny this operation unconditionally.
|
||||
Denied,
|
||||
// Unbounded allow, provided no denied exists.
|
||||
Grant,
|
||||
Deny,
|
||||
// Unbounded allow, provided no deny state exists.
|
||||
// Grant,
|
||||
// This module makes no decisions about this entry.
|
||||
Ignore,
|
||||
// Limit the allowed attr set to this - this doesn't
|
||||
// allow anything, it constrains what might be allowed.
|
||||
Constrain(BTreeSet<&'a str>),
|
||||
// Allow these attributes within constraints.
|
||||
Allow(BTreeSet<&'a str>),
|
||||
// allow anything, it constrains what might be allowed
|
||||
// by a later module.
|
||||
Constrain {
|
||||
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()
|
||||
.filter(|e| {
|
||||
match apply_search_access(ident, related_acp.as_slice(), e) {
|
||||
SearchResult::Denied => false,
|
||||
SearchResult::Deny => false,
|
||||
SearchResult::Grant => true,
|
||||
SearchResult::Allow(allowed_attrs) => {
|
||||
// The allow set constrained.
|
||||
|
@ -401,7 +425,7 @@ pub trait AccessControlsTransaction<'a> {
|
|||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
match apply_search_access(&se.ident, &search_related_acp, &entry) {
|
||||
SearchResult::Denied => {
|
||||
SearchResult::Deny => {
|
||||
None
|
||||
}
|
||||
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
|
||||
// I think we have no limit, but ... william of the future may find a problem with this
|
||||
// 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() {
|
||||
match modify {
|
||||
|
@ -548,27 +573,33 @@ pub trait AccessControlsTransaction<'a> {
|
|||
// 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".
|
||||
requested_classes.extend(v.to_str())
|
||||
requested_pres_classes.extend(v.to_str())
|
||||
}
|
||||
}
|
||||
Modify::Removed(a, v) => {
|
||||
if a == Attribute::Class.as_ref() {
|
||||
requested_classes.extend(v.to_str())
|
||||
requested_rem_classes.extend(v.to_str())
|
||||
}
|
||||
}
|
||||
Modify::Set(a, v) => {
|
||||
if a == Attribute::Class.as_ref() {
|
||||
// flatten to remove the option down to an iterator
|
||||
requested_classes.extend(v.as_iutf8_iter().into_iter().flatten())
|
||||
// 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_rem, "Requested remove set");
|
||||
debug!(?requested_classes, "Requested class set");
|
||||
debug!(?requested_pres, "Requested present attribute set");
|
||||
debug!(?requested_rem, "Requested remove attribute set");
|
||||
debug!(?requested_pres_classes, "Requested present class set");
|
||||
debug!(?requested_rem_classes, "Requested remove class set");
|
||||
|
||||
let sync_agmts = self.get_sync_agreements();
|
||||
|
||||
|
@ -576,9 +607,16 @@ pub trait AccessControlsTransaction<'a> {
|
|||
debug!(entry_id = %e.get_display_id());
|
||||
|
||||
match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) {
|
||||
ModifyResult::Denied => false,
|
||||
ModifyResult::Deny => false,
|
||||
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) {
|
||||
security_error!("requested_pres is not a subset of allowed");
|
||||
security_error!(
|
||||
|
@ -586,23 +624,41 @@ pub trait AccessControlsTransaction<'a> {
|
|||
requested_pres,
|
||||
pres
|
||||
);
|
||||
false
|
||||
} else if !requested_rem.is_subset(&rem) {
|
||||
decision = false
|
||||
};
|
||||
|
||||
if !requested_rem.is_subset(&rem) {
|
||||
security_error!("requested_rem is not a subset of allowed");
|
||||
security_error!("requested_rem: {:?} !⊆ allowed: {:?}", requested_rem, rem);
|
||||
false
|
||||
} else if !requested_classes.is_subset(&cls) {
|
||||
security_error!("requested_classes is not a subset of allowed");
|
||||
decision = false;
|
||||
};
|
||||
|
||||
if !requested_pres_classes.is_subset(&pres_cls) {
|
||||
security_error!("requested_pres_classes is not a subset of allowed");
|
||||
security_error!(
|
||||
"requested_classes: {:?} !⊆ allowed: {:?}",
|
||||
requested_classes,
|
||||
cls
|
||||
"requested_pres_classes: {:?} !⊆ allowed: {:?}",
|
||||
requested_pres_classes,
|
||||
pres_cls
|
||||
);
|
||||
false
|
||||
} else {
|
||||
decision = false;
|
||||
};
|
||||
|
||||
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.");
|
||||
true
|
||||
} // if acc == false
|
||||
}
|
||||
|
||||
// Yield the result
|
||||
decision
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -668,47 +724,55 @@ pub trait AccessControlsTransaction<'a> {
|
|||
})
|
||||
.collect();
|
||||
|
||||
// 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
|
||||
// policy.
|
||||
let requested_classes: BTreeSet<&str> = modlist
|
||||
.iter()
|
||||
.filter_map(|m| match m {
|
||||
let mut requested_pres_classes: BTreeSet<&str> = Default::default();
|
||||
let mut requested_rem_classes: BTreeSet<&str> = Default::default();
|
||||
|
||||
for modify in modlist.iter() {
|
||||
match modify {
|
||||
Modify::Present(a, v) => {
|
||||
if a == Attribute::Class.as_ref() {
|
||||
// Here we have an option<&str> which could mean there is a risk of
|
||||
// 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
|
||||
requested_pres_classes.extend(v.to_str())
|
||||
}
|
||||
}
|
||||
Modify::Removed(a, v) => {
|
||||
if a == Attribute::Class.as_ref() {
|
||||
v.to_str()
|
||||
} else {
|
||||
None
|
||||
requested_rem_classes.extend(v.to_str())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
Modify::Set(a, v) => {
|
||||
if a == Attribute::Class.as_ref() {
|
||||
// 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_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());
|
||||
|
||||
let sync_agmts = self.get_sync_agreements();
|
||||
|
||||
match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) {
|
||||
ModifyResult::Denied => false,
|
||||
ModifyResult::Deny => false,
|
||||
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) {
|
||||
security_error!("requested_pres is not a subset of allowed");
|
||||
security_error!(
|
||||
|
@ -716,23 +780,41 @@ pub trait AccessControlsTransaction<'a> {
|
|||
requested_pres,
|
||||
pres
|
||||
);
|
||||
false
|
||||
} else if !requested_rem.is_subset(&rem) {
|
||||
decision = false
|
||||
};
|
||||
|
||||
if !requested_rem.is_subset(&rem) {
|
||||
security_error!("requested_rem is not a subset of allowed");
|
||||
security_error!("requested_rem: {:?} !⊆ allowed: {:?}", requested_rem, rem);
|
||||
false
|
||||
} else if !requested_classes.is_subset(&cls) {
|
||||
security_error!("requested_classes is not a subset of allowed");
|
||||
decision = false;
|
||||
};
|
||||
|
||||
if !requested_pres_classes.is_subset(&pres_cls) {
|
||||
security_error!("requested_pres_classes is not a subset of allowed");
|
||||
security_error!(
|
||||
"requested_classes: {:?} !⊆ allowed: {:?}",
|
||||
requested_classes,
|
||||
cls
|
||||
requested_pres_classes,
|
||||
pres_cls
|
||||
);
|
||||
false
|
||||
} else {
|
||||
security_access!("passed pres, rem, classes check.");
|
||||
true
|
||||
} // if acc == false
|
||||
decision = false;
|
||||
};
|
||||
|
||||
if !requested_rem_classes.is_subset(&rem_cls) {
|
||||
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
|
||||
let r = entries.iter().all(|e| {
|
||||
match apply_create_access(&ce.ident, related_acp.as_slice(), e) {
|
||||
CreateResult::Denied => false,
|
||||
CreateResult::Deny => false,
|
||||
CreateResult::Grant => true,
|
||||
}
|
||||
});
|
||||
|
@ -836,7 +918,7 @@ pub trait AccessControlsTransaction<'a> {
|
|||
// For each entry
|
||||
let r = entries.iter().all(|e| {
|
||||
match apply_delete_access(&de.ident, related_acp.as_slice(), e) {
|
||||
DeleteResult::Denied => false,
|
||||
DeleteResult::Deny => false,
|
||||
DeleteResult::Grant => true,
|
||||
}
|
||||
});
|
||||
|
@ -925,7 +1007,7 @@ pub trait AccessControlsTransaction<'a> {
|
|||
) -> AccessEffectivePermission {
|
||||
// == search ==
|
||||
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::Allow(allowed_attrs) => {
|
||||
// Bound by requested attrs?
|
||||
|
@ -934,14 +1016,30 @@ pub trait AccessControlsTransaction<'a> {
|
|||
};
|
||||
|
||||
// == 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) {
|
||||
ModifyResult::Denied => (Access::Denied, Access::Denied, AccessClass::Denied),
|
||||
ModifyResult::Grant => (Access::Grant, Access::Grant, AccessClass::Grant),
|
||||
ModifyResult::Allow { pres, rem, cls } => (
|
||||
ModifyResult::Deny => (
|
||||
Access::Deny,
|
||||
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(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 = match delete_status {
|
||||
DeleteResult::Denied => false,
|
||||
DeleteResult::Deny => false,
|
||||
DeleteResult::Grant => true,
|
||||
};
|
||||
|
||||
|
@ -960,7 +1058,8 @@ pub trait AccessControlsTransaction<'a> {
|
|||
search: search_effective,
|
||||
modify_pres,
|
||||
modify_rem,
|
||||
modify_class,
|
||||
modify_pres_class,
|
||||
modify_rem_class,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2166,6 +2265,8 @@ mod tests {
|
|||
"name class",
|
||||
// And the class allowed is account
|
||||
EntryClass::Account.into(),
|
||||
// And the class allowed is account
|
||||
EntryClass::Account.into(),
|
||||
);
|
||||
// Allow member, class is group. IE not account
|
||||
let acp_deny = AccessControlModify::from_raw(
|
||||
|
@ -2182,8 +2283,8 @@ mod tests {
|
|||
"member class",
|
||||
// Allow rem name and class
|
||||
"member class",
|
||||
// And the class allowed is account
|
||||
"group",
|
||||
EntryClass::Group.into(),
|
||||
EntryClass::Group.into(),
|
||||
);
|
||||
// Does not have a pres or rem class in attrs
|
||||
let acp_no_class = AccessControlModify::from_raw(
|
||||
|
@ -2201,7 +2302,8 @@ mod tests {
|
|||
// Allow rem name and class
|
||||
"name class",
|
||||
// And the class allowed is NOT an account ...
|
||||
"group",
|
||||
EntryClass::Group.into(),
|
||||
EntryClass::Group.into(),
|
||||
);
|
||||
|
||||
// Test allowed pres
|
||||
|
@ -2287,6 +2389,7 @@ mod tests {
|
|||
"name class",
|
||||
// And the class allowed is account
|
||||
EntryClass::Account.into(),
|
||||
EntryClass::Account.into(),
|
||||
);
|
||||
|
||||
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]),
|
||||
modify_pres: 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(),
|
||||
EntryClass::Object.into(),
|
||||
EntryClass::Object.into(),
|
||||
)],
|
||||
&r_set,
|
||||
vec![AccessEffectivePermission {
|
||||
|
@ -2656,7 +2761,8 @@ mod tests {
|
|||
search: Access::Allow(BTreeSet::new()),
|
||||
modify_pres: 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),
|
||||
// And the class allowed is account, we don't use it though.
|
||||
EntryClass::Account.into(),
|
||||
EntryClass::Account.into(),
|
||||
);
|
||||
|
||||
// NOTE! Syntax doesn't matter here, we just need to assert if the attr exists
|
||||
|
@ -3296,6 +3403,7 @@ mod tests {
|
|||
"name class",
|
||||
// And the class allowed is account
|
||||
EntryClass::Account.into(),
|
||||
EntryClass::Account.into(),
|
||||
);
|
||||
|
||||
// Test allowed pres
|
||||
|
@ -3424,4 +3532,185 @@ mod tests {
|
|||
// Finally test it!
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
use crate::prelude::*;
|
||||
use hashbrown::HashMap;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::profiles::{
|
||||
AccessControlModify, AccessControlModifyResolved, AccessControlReceiverCondition,
|
||||
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;
|
||||
|
||||
pub(super) enum ModifyResult<'a> {
|
||||
Denied,
|
||||
Deny,
|
||||
Grant,
|
||||
Allow {
|
||||
pres: 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> {
|
||||
let mut denied = false;
|
||||
let mut grant = false;
|
||||
|
||||
let mut constrain_pres = BTreeSet::default();
|
||||
let mut allow_pres = BTreeSet::default();
|
||||
let mut constrain_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.
|
||||
// - 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.
|
||||
|
||||
match modify_ident_test(ident) {
|
||||
AccessResult::Denied => denied = true,
|
||||
AccessResult::Grant => grant = true,
|
||||
AccessResult::Ignore => {}
|
||||
AccessResult::Constrain(mut set) => constrain_pres.append(&mut set),
|
||||
AccessResult::Allow(mut set) => allow_pres.append(&mut set),
|
||||
AccessBasicResult::Deny => denied = true,
|
||||
AccessBasicResult::Grant => grant = true,
|
||||
AccessBasicResult::Ignore => {}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Check with protected if we should proceed.
|
||||
|
||||
// If it's a sync entry, constrain it.
|
||||
match modify_sync_constrain(ident, entry, sync_agreements) {
|
||||
AccessResult::Denied => denied = true,
|
||||
AccessResult::Constrain(mut set) => {
|
||||
constrain_rem.extend(set.iter().cloned());
|
||||
constrain_pres.append(&mut set)
|
||||
AccessModResult::Deny => denied = true,
|
||||
AccessModResult::Constrain {
|
||||
mut pres_attr,
|
||||
mut rem_attr,
|
||||
..
|
||||
} => {
|
||||
constrain_rem.append(&mut rem_attr);
|
||||
constrain_pres.append(&mut pres_attr);
|
||||
}
|
||||
// Can't grant.
|
||||
AccessResult::Grant |
|
||||
// AccessModResult::Grant |
|
||||
// Can't allow
|
||||
AccessResult::Allow(_) |
|
||||
AccessResult::Ignore => {}
|
||||
AccessModResult::Allow { .. } | AccessModResult::Ignore => {}
|
||||
}
|
||||
|
||||
// Setup the acp's here
|
||||
|
@ -122,35 +156,27 @@ pub(super) fn apply_modify_access<'a>(
|
|||
.collect();
|
||||
|
||||
match modify_pres_test(scoped_acp.as_slice()) {
|
||||
AccessResult::Denied => denied = true,
|
||||
AccessModResult::Deny => denied = true,
|
||||
// Can never return a unilateral grant.
|
||||
AccessResult::Grant => {}
|
||||
AccessResult::Ignore => {}
|
||||
AccessResult::Constrain(mut set) => constrain_pres.append(&mut set),
|
||||
AccessResult::Allow(mut set) => allow_pres.append(&mut set),
|
||||
}
|
||||
|
||||
match modify_rem_test(scoped_acp.as_slice()) {
|
||||
AccessResult::Denied => denied = true,
|
||||
// Can never return a unilateral grant.
|
||||
AccessResult::Grant => {}
|
||||
AccessResult::Ignore => {}
|
||||
AccessResult::Constrain(mut set) => constrain_rem.append(&mut set),
|
||||
AccessResult::Allow(mut set) => allow_rem.append(&mut set),
|
||||
}
|
||||
|
||||
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),
|
||||
// AccessModResult::Grant => {}
|
||||
AccessModResult::Ignore => {}
|
||||
AccessModResult::Constrain { .. } => {}
|
||||
AccessModResult::Allow {
|
||||
mut pres_attr,
|
||||
mut rem_attr,
|
||||
mut pres_class,
|
||||
mut rem_class,
|
||||
} => {
|
||||
allow_pres.append(&mut pres_attr);
|
||||
allow_rem.append(&mut rem_attr);
|
||||
allow_pres_cls.append(&mut pres_class);
|
||||
allow_rem_cls.append(&mut rem_class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if denied {
|
||||
ModifyResult::Denied
|
||||
ModifyResult::Deny
|
||||
} else if grant {
|
||||
ModifyResult::Grant
|
||||
} else {
|
||||
|
@ -168,31 +194,48 @@ pub(super) fn apply_modify_access<'a>(
|
|||
allow_rem
|
||||
};
|
||||
|
||||
let allowed_cls = if !constrain_cls.is_empty() {
|
||||
let mut allowed_pres_cls = if !constrain_pres_cls.is_empty() {
|
||||
// bit_and
|
||||
&constrain_cls & &allow_cls
|
||||
&constrain_pres_cls & &allow_pres_cls
|
||||
} 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 {
|
||||
pres: allowed_pres,
|
||||
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 {
|
||||
IdentType::Internal => {
|
||||
trace!("Internal operation, bypassing access check");
|
||||
// No need to check ACS
|
||||
return AccessResult::Grant;
|
||||
return AccessBasicResult::Grant;
|
||||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_critical!("Blocking sync check");
|
||||
return AccessResult::Denied;
|
||||
return AccessBasicResult::Deny;
|
||||
}
|
||||
IdentType::User(_) => {}
|
||||
};
|
||||
|
@ -201,53 +244,56 @@ fn modify_ident_test(ident: &Identity) -> AccessResult {
|
|||
match ident.access_scope() {
|
||||
AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to modify");
|
||||
return AccessResult::Denied;
|
||||
return AccessBasicResult::Deny;
|
||||
}
|
||||
AccessScope::ReadWrite => {
|
||||
// As you were
|
||||
}
|
||||
};
|
||||
|
||||
AccessResult::Ignore
|
||||
AccessBasicResult::Ignore
|
||||
}
|
||||
|
||||
fn modify_pres_test(scoped_acp: &[&AccessControlModify]) -> AccessResult {
|
||||
let allowed_pres: BTreeSet<Attribute> = scoped_acp
|
||||
fn modify_pres_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessModResult<'a> {
|
||||
let pres_attr: BTreeSet<Attribute> = scoped_acp
|
||||
.iter()
|
||||
.flat_map(|acp| acp.presattrs.iter().cloned())
|
||||
.collect();
|
||||
AccessResult::Allow(allowed_pres)
|
||||
}
|
||||
|
||||
fn modify_rem_test(scoped_acp: &[&AccessControlModify]) -> AccessResult {
|
||||
let allowed_rem: BTreeSet<Attribute> = scoped_acp
|
||||
let rem_attr: BTreeSet<Attribute> = scoped_acp
|
||||
.iter()
|
||||
.flat_map(|acp| acp.remattrs.iter().cloned())
|
||||
.collect();
|
||||
AccessResult::Allow(allowed_rem)
|
||||
}
|
||||
|
||||
// TODO: Should this be reverted to the Str borrow method? Or do we try to change
|
||||
// to EntryClass?
|
||||
fn modify_cls_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessResultClass<'a> {
|
||||
let allowed_classes: BTreeSet<&'a str> = scoped_acp
|
||||
let pres_class: BTreeSet<&'a str> = scoped_acp
|
||||
.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();
|
||||
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,
|
||||
entry: &Arc<EntrySealedCommitted>,
|
||||
sync_agreements: &HashMap<Uuid, BTreeSet<Attribute>>,
|
||||
) -> AccessResult {
|
||||
) -> AccessModResult<'a> {
|
||||
match &ident.origin {
|
||||
IdentType::Internal => AccessResult::Ignore,
|
||||
IdentType::Internal => AccessModResult::Ignore,
|
||||
IdentType::Synch(_) => {
|
||||
// Allowed to mod sync objects. Later we'll probably need to check the limits of what
|
||||
// it can do if we go that way.
|
||||
AccessResult::Ignore
|
||||
AccessModResult::Ignore
|
||||
}
|
||||
IdentType::User(_) => {
|
||||
// We need to meet these conditions.
|
||||
|
@ -259,7 +305,7 @@ fn modify_sync_constrain(
|
|||
.unwrap_or(false);
|
||||
|
||||
if !is_sync {
|
||||
return AccessResult::Ignore;
|
||||
return AccessModResult::Ignore;
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
AccessResult::Constrain(set)
|
||||
AccessModResult::Constrain {
|
||||
pres_attr: set.clone(),
|
||||
rem_attr: set,
|
||||
pres_cls: None,
|
||||
rem_cls: None,
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -266,9 +266,10 @@ pub struct AccessControlModifyResolved<'a> {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct AccessControlModify {
|
||||
pub acp: AccessControlProfile,
|
||||
pub classes: Vec<AttrString>,
|
||||
pub presattrs: Vec<Attribute>,
|
||||
pub remattrs: Vec<Attribute>,
|
||||
pub pres_classes: Vec<AttrString>,
|
||||
pub rem_classes: Vec<AttrString>,
|
||||
}
|
||||
|
||||
impl AccessControlModify {
|
||||
|
@ -293,14 +294,25 @@ impl AccessControlModify {
|
|||
.map(|i| i.map(Attribute::from).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let classes = value
|
||||
let classes: Vec<AttrString> = value
|
||||
.get_ava_iter_iutf8(Attribute::AcpModifyClass)
|
||||
.map(|i| i.map(AttrString::from).collect())
|
||||
.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 {
|
||||
acp: AccessControlProfile::try_from(qs, value)?,
|
||||
classes,
|
||||
pres_classes,
|
||||
rem_classes,
|
||||
presattrs,
|
||||
remattrs,
|
||||
})
|
||||
|
@ -316,7 +328,8 @@ impl AccessControlModify {
|
|||
targetscope: Filter<FilterValid>,
|
||||
presattrs: &str,
|
||||
remattrs: &str,
|
||||
classes: &str,
|
||||
pres_classes: &str,
|
||||
rem_classes: &str,
|
||||
) -> Self {
|
||||
AccessControlModify {
|
||||
acp: AccessControlProfile {
|
||||
|
@ -325,7 +338,14 @@ impl AccessControlModify {
|
|||
receiver: AccessControlReceiver::Group(btreeset!(receiver)),
|
||||
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(),
|
||||
remattrs: remattrs.split_whitespace().map(Attribute::from).collect(),
|
||||
}
|
||||
|
@ -340,7 +360,8 @@ impl AccessControlModify {
|
|||
target: AccessControlTarget,
|
||||
presattrs: &str,
|
||||
remattrs: &str,
|
||||
classes: &str,
|
||||
pres_classes: &str,
|
||||
rem_classes: &str,
|
||||
) -> Self {
|
||||
AccessControlModify {
|
||||
acp: AccessControlProfile {
|
||||
|
@ -349,7 +370,14 @@ impl AccessControlModify {
|
|||
receiver: AccessControlReceiver::EntryManager,
|
||||
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(),
|
||||
remattrs: remattrs.split_whitespace().map(Attribute::from).collect(),
|
||||
}
|
||||
|
|
83
server/lib/src/server/access/protected.rs
Normal file
83
server/lib/src/server/access/protected.rs
Normal 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()))
|
||||
});
|
|
@ -4,11 +4,11 @@ use std::collections::BTreeSet;
|
|||
use super::profiles::{
|
||||
AccessControlReceiverCondition, AccessControlSearchResolved, AccessControlTargetCondition,
|
||||
};
|
||||
use super::AccessResult;
|
||||
use super::AccessSrchResult;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) enum SearchResult {
|
||||
Denied,
|
||||
Deny,
|
||||
Grant,
|
||||
Allow(BTreeSet<Attribute>),
|
||||
}
|
||||
|
@ -23,32 +23,32 @@ pub(super) fn apply_search_access(
|
|||
// that.
|
||||
let mut denied = false;
|
||||
let mut grant = false;
|
||||
let mut constrain = BTreeSet::default();
|
||||
let constrain = BTreeSet::default();
|
||||
let mut allow = BTreeSet::default();
|
||||
|
||||
// The access control profile
|
||||
match search_filter_entry(ident, related_acp, entry) {
|
||||
AccessResult::Denied => denied = true,
|
||||
AccessResult::Grant => grant = true,
|
||||
AccessResult::Ignore => {}
|
||||
AccessResult::Constrain(mut set) => constrain.append(&mut set),
|
||||
AccessResult::Allow(mut set) => allow.append(&mut set),
|
||||
AccessSrchResult::Deny => denied = true,
|
||||
AccessSrchResult::Grant => grant = true,
|
||||
AccessSrchResult::Ignore => {}
|
||||
// AccessSrchResult::Constrain { mut attr } => constrain.append(&mut attr),
|
||||
AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
|
||||
};
|
||||
|
||||
match search_oauth2_filter_entry(ident, entry) {
|
||||
AccessResult::Denied => denied = true,
|
||||
AccessResult::Grant => grant = true,
|
||||
AccessResult::Ignore => {}
|
||||
AccessResult::Constrain(mut set) => constrain.append(&mut set),
|
||||
AccessResult::Allow(mut set) => allow.append(&mut set),
|
||||
AccessSrchResult::Deny => denied = true,
|
||||
AccessSrchResult::Grant => grant = true,
|
||||
AccessSrchResult::Ignore => {}
|
||||
// AccessSrchResult::Constrain { mut attr } => constrain.append(&mut attr),
|
||||
AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
|
||||
};
|
||||
|
||||
match search_sync_account_filter_entry(ident, entry) {
|
||||
AccessResult::Denied => denied = true,
|
||||
AccessResult::Grant => grant = true,
|
||||
AccessResult::Ignore => {}
|
||||
AccessResult::Constrain(mut set) => constrain.append(&mut set),
|
||||
AccessResult::Allow(mut set) => allow.append(&mut set),
|
||||
AccessSrchResult::Deny => denied = true,
|
||||
AccessSrchResult::Grant => grant = true,
|
||||
AccessSrchResult::Ignore => {}
|
||||
// AccessSrchResult::Constrain{ mut attr } => constrain.append(&mut attr),
|
||||
AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
|
||||
};
|
||||
|
||||
// We'll add more modules later.
|
||||
|
@ -56,7 +56,7 @@ pub(super) fn apply_search_access(
|
|||
// Now finalise the decision.
|
||||
|
||||
if denied {
|
||||
SearchResult::Denied
|
||||
SearchResult::Deny
|
||||
} else if grant {
|
||||
SearchResult::Grant
|
||||
} else {
|
||||
|
@ -74,17 +74,17 @@ fn search_filter_entry(
|
|||
ident: &Identity,
|
||||
related_acp: &[AccessControlSearchResolved],
|
||||
entry: &Arc<EntrySealedCommitted>,
|
||||
) -> AccessResult {
|
||||
) -> AccessSrchResult {
|
||||
// If this is an internal search, return our working set.
|
||||
match &ident.origin {
|
||||
IdentType::Internal => {
|
||||
trace!(uuid = ?entry.get_display_id(), "Internal operation, bypassing access check");
|
||||
// No need to check ACS
|
||||
return AccessResult::Grant;
|
||||
return AccessSrchResult::Grant;
|
||||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_debug!(uuid = ?entry.get_display_id(), "Blocking sync check");
|
||||
return AccessResult::Denied;
|
||||
return AccessSrchResult::Deny;
|
||||
}
|
||||
IdentType::User(_) => {}
|
||||
};
|
||||
|
@ -95,7 +95,7 @@ fn search_filter_entry(
|
|||
security_debug!(
|
||||
"denied ❌ - identity access scope 'Synchronise' is not permitted to search"
|
||||
);
|
||||
return AccessResult::Denied;
|
||||
return AccessSrchResult::Deny;
|
||||
}
|
||||
AccessScope::ReadOnly | AccessScope::ReadWrite => {
|
||||
// As you were
|
||||
|
@ -161,16 +161,21 @@ fn search_filter_entry(
|
|||
.flatten()
|
||||
.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 {
|
||||
IdentType::Internal | IdentType::Synch(_) => AccessResult::Ignore,
|
||||
IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
|
||||
IdentType::User(iuser) => {
|
||||
if iuser.entry.get_uuid() == UUID_ANONYMOUS {
|
||||
debug!("Anonymous can't access OAuth2 entries, ignoring");
|
||||
return AccessResult::Ignore;
|
||||
return AccessSrchResult::Ignore;
|
||||
}
|
||||
|
||||
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 {
|
||||
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!(
|
||||
Attribute::Class,
|
||||
Attribute::DisplayName,
|
||||
Attribute::Uuid,
|
||||
Attribute::Name,
|
||||
Attribute::OAuth2RsOriginLanding,
|
||||
Attribute::Image
|
||||
));
|
||||
return AccessSrchResult::Allow {
|
||||
attr: btreeset!(
|
||||
Attribute::Class,
|
||||
Attribute::DisplayName,
|
||||
Attribute::Uuid,
|
||||
Attribute::Name,
|
||||
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(
|
||||
ident: &Identity,
|
||||
entry: &Arc<EntrySealedCommitted>,
|
||||
) -> AccessResult {
|
||||
) -> AccessSrchResult {
|
||||
match &ident.origin {
|
||||
IdentType::Internal | IdentType::Synch(_) => AccessResult::Ignore,
|
||||
IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
|
||||
IdentType::User(iuser) => {
|
||||
// Is the user a synced object?
|
||||
let is_user_sync_account = iuser
|
||||
|
@ -244,16 +251,18 @@ fn search_sync_account_filter_entry(
|
|||
// We finally got here!
|
||||
security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a synchronised account from this sync account");
|
||||
|
||||
return AccessResult::Allow(btreeset!(
|
||||
Attribute::Class,
|
||||
Attribute::Uuid,
|
||||
Attribute::SyncCredentialPortal
|
||||
));
|
||||
return AccessSrchResult::Allow {
|
||||
attr: btreeset!(
|
||||
Attribute::Class,
|
||||
Attribute::Uuid,
|
||||
Attribute::SyncCredentialPortal
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall through
|
||||
AccessResult::Ignore
|
||||
AccessSrchResult::Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue