mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
20230121 access improvement (#1345)
This commit is contained in:
parent
251feac7cb
commit
723c428e37
|
@ -885,7 +885,10 @@ fn config_security_checks(cfg_path: &Path) -> bool {
|
|||
let cfg_meta = match metadata(&cfg_path) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Unable to read metadata for '{}' during security checks - {:?}", cfg_path_str, e);
|
||||
error!(
|
||||
"Unable to read metadata for '{}' during security checks - {:?}",
|
||||
cfg_path_str, e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -44,7 +44,6 @@ pub struct TlsConfiguration {
|
|||
pub key: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
pub bindaddress: Option<String>,
|
||||
|
|
|
@ -20,6 +20,7 @@ use crate::credential::{BackupCodes, Credential};
|
|||
use crate::idm::account::Account;
|
||||
use crate::idm::server::{IdmServerCredUpdateTransaction, IdmServerProxyWriteTransaction};
|
||||
use crate::prelude::*;
|
||||
use crate::server::access::Access;
|
||||
use crate::utils::{backup_code_from_random, readable_password_from_random, uuid_from_duration};
|
||||
use crate::value::IntentTokenState;
|
||||
|
||||
|
@ -344,10 +345,25 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
return Err(OperationError::InvalidEntryState);
|
||||
}
|
||||
|
||||
if !eperm.search.contains("primary_credential")
|
||||
|| !eperm.modify_pres.contains("primary_credential")
|
||||
|| !eperm.modify_rem.contains("primary_credential")
|
||||
{
|
||||
let eperm_search_primary_cred = match &eperm.search {
|
||||
Access::Denied => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains("primary_credential"),
|
||||
};
|
||||
|
||||
let eperm_mod_primary_cred = match &eperm.modify_pres {
|
||||
Access::Denied => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains("primary_credential"),
|
||||
};
|
||||
|
||||
let eperm_rem_primary_cred = match &eperm.modify_rem {
|
||||
Access::Denied => false,
|
||||
Access::Grant => true,
|
||||
Access::Allow(attrs) => attrs.contains("primary_credential"),
|
||||
};
|
||||
|
||||
if !eperm_search_primary_cred || !eperm_mod_primary_cred || !eperm_rem_primary_cred {
|
||||
security_info!(
|
||||
"Requestor {} does not have permission to update credentials of {}",
|
||||
ident,
|
||||
|
|
138
kanidmd/lib/src/server/access/create.rs
Normal file
138
kanidmd/lib/src/server/access/create.rs
Normal file
|
@ -0,0 +1,138 @@
|
|||
use super::profiles::AccessControlCreate;
|
||||
use crate::filter::FilterValidResolved;
|
||||
use crate::prelude::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
pub(super) enum CreateResult {
|
||||
Denied,
|
||||
Grant,
|
||||
}
|
||||
|
||||
enum IResult {
|
||||
Denied,
|
||||
Grant,
|
||||
Ignore,
|
||||
}
|
||||
|
||||
pub(super) fn apply_create_access<'a>(
|
||||
ident: &Identity,
|
||||
related_acp: &'a [(&AccessControlCreate, Filter<FilterValidResolved>)],
|
||||
entry: &'a Entry<EntryInit, EntryNew>,
|
||||
) -> CreateResult {
|
||||
let mut denied = false;
|
||||
let mut grant = false;
|
||||
|
||||
match create_filter_entry(ident, related_acp, entry) {
|
||||
IResult::Denied => denied = true,
|
||||
IResult::Grant => grant = true,
|
||||
IResult::Ignore => {}
|
||||
}
|
||||
|
||||
if denied {
|
||||
// Something explicitly said no.
|
||||
CreateResult::Denied
|
||||
} else if grant {
|
||||
// Something said yes
|
||||
CreateResult::Grant
|
||||
} else {
|
||||
// Nothing said yes.
|
||||
CreateResult::Denied
|
||||
}
|
||||
}
|
||||
|
||||
fn create_filter_entry<'a>(
|
||||
ident: &Identity,
|
||||
related_acp: &'a [(&AccessControlCreate, Filter<FilterValidResolved>)],
|
||||
entry: &'a Entry<EntryInit, EntryNew>,
|
||||
) -> IResult {
|
||||
match &ident.origin {
|
||||
IdentType::Internal => {
|
||||
trace!("Internal operation, bypassing access check");
|
||||
// No need to check ACS
|
||||
return IResult::Grant;
|
||||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_critical!("Blocking sync check");
|
||||
return IResult::Denied;
|
||||
}
|
||||
IdentType::User(_) => {}
|
||||
};
|
||||
info!(event = %ident, "Access check for create event");
|
||||
|
||||
match ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to create");
|
||||
return IResult::Denied;
|
||||
}
|
||||
AccessScope::ReadWrite => {
|
||||
// As you were
|
||||
}
|
||||
};
|
||||
|
||||
// Build the set of requested classes and attrs here.
|
||||
let create_attrs: BTreeSet<&str> = entry.get_ava_names().collect();
|
||||
// If this is empty, we make an empty set, which is fine because
|
||||
// the empty class set despite matching is_subset, will have the
|
||||
// following effect:
|
||||
// * there is no class on entry, so schema will fail
|
||||
// * plugin-base will add object to give a class, but excess
|
||||
// attrs will cause fail (could this be a weakness?)
|
||||
// * class is a "may", so this could be empty in the rules, so
|
||||
// if the accr is empty this would not be a true subset,
|
||||
// so this would "fail", but any content in the accr would
|
||||
// have to be validated.
|
||||
//
|
||||
// I still think if this is None, we should just fail here ...
|
||||
// because it shouldn't be possible to match.
|
||||
|
||||
let create_classes: BTreeSet<&str> = match entry.get_ava_iter_iutf8("class") {
|
||||
Some(s) => s.collect(),
|
||||
None => {
|
||||
admin_error!("Class set failed to build - corrupted entry?");
|
||||
return IResult::Denied;
|
||||
}
|
||||
};
|
||||
|
||||
// Find the set of related acps for this entry.
|
||||
//
|
||||
// For each "created" entry.
|
||||
// If the created entry is 100% allowed by this acp
|
||||
// IE: all attrs to be created AND classes match classes
|
||||
// allow
|
||||
// if no acp allows, fail operation.
|
||||
let allow = related_acp.iter().any(|(accr, f_res)| {
|
||||
// Check to see if allowed.
|
||||
if entry.entry_match_no_index(f_res) {
|
||||
security_access!(?entry, acs = ?accr, "entry matches acs");
|
||||
// It matches, so now we have to check attrs and classes.
|
||||
// Remember, we have to match ALL requested attrs
|
||||
// and classes to pass!
|
||||
let allowed_attrs: BTreeSet<&str> = accr.attrs.iter().map(|s| s.as_str()).collect();
|
||||
let allowed_classes: BTreeSet<&str> = accr.classes.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
if !create_attrs.is_subset(&allowed_attrs) {
|
||||
security_access!("create_attrs is not a subset of allowed");
|
||||
security_access!("{:?} !⊆ {:?}", create_attrs, allowed_attrs);
|
||||
return false;
|
||||
}
|
||||
if !create_classes.is_subset(&allowed_classes) {
|
||||
security_access!("create_classes is not a subset of allowed");
|
||||
security_access!("{:?} !⊆ {:?}", create_classes, allowed_classes);
|
||||
return false;
|
||||
}
|
||||
security_access!("passed");
|
||||
|
||||
true
|
||||
} else {
|
||||
trace!(?entry, acs = %accr.acp.name, "entry DOES NOT match acs");
|
||||
// Does not match, fail this rule.
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if allow {
|
||||
IResult::Grant
|
||||
} else {
|
||||
IResult::Ignore
|
||||
}
|
||||
}
|
97
kanidmd/lib/src/server/access/delete.rs
Normal file
97
kanidmd/lib/src/server/access/delete.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use super::profiles::AccessControlDelete;
|
||||
use crate::filter::FilterValidResolved;
|
||||
use crate::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) enum DeleteResult {
|
||||
Denied,
|
||||
Grant,
|
||||
}
|
||||
|
||||
enum IResult {
|
||||
Denied,
|
||||
Grant,
|
||||
Ignore,
|
||||
}
|
||||
|
||||
pub(super) fn apply_delete_access<'a>(
|
||||
ident: &Identity,
|
||||
related_acp: &'a [(&AccessControlDelete, Filter<FilterValidResolved>)],
|
||||
entry: &'a Arc<EntrySealedCommitted>,
|
||||
) -> DeleteResult {
|
||||
let mut denied = false;
|
||||
let mut grant = false;
|
||||
|
||||
match delete_filter_entry(ident, related_acp, entry) {
|
||||
IResult::Denied => denied = true,
|
||||
IResult::Grant => grant = true,
|
||||
IResult::Ignore => {}
|
||||
}
|
||||
|
||||
if denied {
|
||||
// Something explicitly said no.
|
||||
DeleteResult::Denied
|
||||
} else if grant {
|
||||
// Something said yes
|
||||
DeleteResult::Grant
|
||||
} else {
|
||||
// Nothing said yes.
|
||||
DeleteResult::Denied
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_filter_entry<'a>(
|
||||
ident: &Identity,
|
||||
related_acp: &'a [(&AccessControlDelete, Filter<FilterValidResolved>)],
|
||||
entry: &'a Arc<EntrySealedCommitted>,
|
||||
) -> IResult {
|
||||
match &ident.origin {
|
||||
IdentType::Internal => {
|
||||
trace!("Internal operation, bypassing access check");
|
||||
// No need to check ACS
|
||||
return IResult::Grant;
|
||||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_critical!("Blocking sync check");
|
||||
return IResult::Denied;
|
||||
}
|
||||
IdentType::User(_) => {}
|
||||
};
|
||||
info!(event = %ident, "Access check for delete event");
|
||||
|
||||
match ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to delete");
|
||||
return IResult::Denied;
|
||||
}
|
||||
AccessScope::ReadWrite => {
|
||||
// As you were
|
||||
}
|
||||
};
|
||||
|
||||
let allow = related_acp.iter().any(|(acd, f_res)| {
|
||||
if entry.entry_match_no_index(f_res) {
|
||||
security_access!(
|
||||
entry_uuid = ?entry.get_uuid(),
|
||||
acs = %acd.acp.name,
|
||||
"entry matches acs"
|
||||
);
|
||||
// It matches, so we can delete this!
|
||||
security_access!("passed");
|
||||
true
|
||||
} else {
|
||||
trace!(
|
||||
"entry {:?} DOES NOT match acs {}",
|
||||
entry.get_uuid(),
|
||||
acd.acp.name
|
||||
);
|
||||
// Does not match, fail.
|
||||
false
|
||||
} // else
|
||||
}); // any related_acp
|
||||
if allow {
|
||||
IResult::Grant
|
||||
} else {
|
||||
IResult::Ignore
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
169
kanidmd/lib/src/server/access/modify.rs
Normal file
169
kanidmd/lib/src/server/access/modify.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
use crate::prelude::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::profiles::AccessControlModify;
|
||||
use super::AccessResult;
|
||||
use crate::filter::FilterValidResolved;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) enum ModifyResult<'a> {
|
||||
Denied,
|
||||
Grant,
|
||||
Allow {
|
||||
pres: BTreeSet<&'a str>,
|
||||
rem: BTreeSet<&'a str>,
|
||||
cls: BTreeSet<&'a str>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) fn apply_modify_access<'a>(
|
||||
ident: &Identity,
|
||||
related_acp: &'a [(&AccessControlModify, Filter<FilterValidResolved>)],
|
||||
entry: &'a Arc<EntrySealedCommitted>,
|
||||
) -> 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();
|
||||
|
||||
// run each module. These have to be broken down further due to modify
|
||||
// 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),
|
||||
}
|
||||
|
||||
if !grant && !denied {
|
||||
// Setup the acp's here
|
||||
let scoped_acp: Vec<&AccessControlModify> = related_acp
|
||||
.iter()
|
||||
.filter_map(|(acm, f_res)| {
|
||||
if entry.entry_match_no_index(f_res) {
|
||||
Some(*acm)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
match modify_pres_test(scoped_acp.as_slice()) {
|
||||
AccessResult::Denied => 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()) {
|
||||
AccessResult::Denied => denied = true,
|
||||
// Can never return a unilateral grant.
|
||||
AccessResult::Grant => {}
|
||||
AccessResult::Ignore => {}
|
||||
AccessResult::Constrain(mut set) => constrain_cls.append(&mut set),
|
||||
AccessResult::Allow(mut set) => allow_cls.append(&mut set),
|
||||
}
|
||||
}
|
||||
|
||||
if denied {
|
||||
ModifyResult::Denied
|
||||
} else if grant {
|
||||
ModifyResult::Grant
|
||||
} else {
|
||||
let allowed_pres = if !constrain_pres.is_empty() {
|
||||
// bit_and
|
||||
&constrain_pres & &allow_pres
|
||||
} else {
|
||||
allow_pres
|
||||
};
|
||||
|
||||
let allowed_rem = if !constrain_rem.is_empty() {
|
||||
// bit_and
|
||||
&constrain_rem & &allow_rem
|
||||
} else {
|
||||
allow_rem
|
||||
};
|
||||
|
||||
let allowed_cls = if !constrain_cls.is_empty() {
|
||||
// bit_and
|
||||
&constrain_cls & &allow_cls
|
||||
} else {
|
||||
allow_cls
|
||||
};
|
||||
|
||||
ModifyResult::Allow {
|
||||
pres: allowed_pres,
|
||||
rem: allowed_rem,
|
||||
cls: allowed_cls,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn modify_ident_test<'a>(ident: &Identity) -> AccessResult<'a> {
|
||||
match &ident.origin {
|
||||
IdentType::Internal => {
|
||||
trace!("Internal operation, bypassing access check");
|
||||
// No need to check ACS
|
||||
return AccessResult::Grant;
|
||||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_critical!("Blocking sync check");
|
||||
return AccessResult::Denied;
|
||||
}
|
||||
IdentType::User(_) => {}
|
||||
};
|
||||
info!(event = %ident, "Access check for modify event");
|
||||
|
||||
match ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to modify");
|
||||
return AccessResult::Denied;
|
||||
}
|
||||
AccessScope::ReadWrite => {
|
||||
// As you were
|
||||
}
|
||||
};
|
||||
|
||||
AccessResult::Ignore
|
||||
}
|
||||
|
||||
fn modify_pres_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessResult<'a> {
|
||||
let allowed_pres: BTreeSet<&str> = scoped_acp
|
||||
.iter()
|
||||
.flat_map(|acp| acp.presattrs.iter().map(|v| v.as_str()))
|
||||
.collect();
|
||||
AccessResult::Allow(allowed_pres)
|
||||
}
|
||||
|
||||
fn modify_rem_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessResult<'a> {
|
||||
let allowed_rem: BTreeSet<&str> = scoped_acp
|
||||
.iter()
|
||||
.flat_map(|acp| acp.remattrs.iter().map(|v| v.as_str()))
|
||||
.collect();
|
||||
AccessResult::Allow(allowed_rem)
|
||||
}
|
||||
|
||||
fn modify_cls_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessResult<'a> {
|
||||
let allowed_classes: BTreeSet<&str> = scoped_acp
|
||||
.iter()
|
||||
.flat_map(|acp| acp.classes.iter().map(|v| v.as_str()))
|
||||
.collect();
|
||||
AccessResult::Allow(allowed_classes)
|
||||
}
|
331
kanidmd/lib/src/server/access/profiles.rs
Normal file
331
kanidmd/lib/src/server/access/profiles.rs
Normal file
|
@ -0,0 +1,331 @@
|
|||
use crate::prelude::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::filter::{Filter, FilterValid};
|
||||
|
||||
use kanidm_proto::v1::Filter as ProtoFilter;
|
||||
|
||||
// =========================================================================
|
||||
// PARSE ENTRY TO ACP, AND ACP MANAGEMENT
|
||||
// =========================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessControlSearch {
|
||||
pub acp: AccessControlProfile,
|
||||
pub attrs: BTreeSet<AttrString>,
|
||||
}
|
||||
|
||||
impl AccessControlSearch {
|
||||
pub fn try_from(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
if !value.attribute_equality("class", &PVCLASS_ACS) {
|
||||
admin_error!("class access_control_search not present.");
|
||||
return Err(OperationError::InvalidAcpState(
|
||||
"Missing access_control_search".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let attrs = value
|
||||
.get_ava_iter_iutf8("acp_search_attr")
|
||||
.ok_or_else(|| {
|
||||
admin_error!("Missing acp_search_attr");
|
||||
OperationError::InvalidAcpState("Missing acp_search_attr".to_string())
|
||||
})?
|
||||
.map(AttrString::from)
|
||||
.collect();
|
||||
|
||||
let acp = AccessControlProfile::try_from(qs, value)?;
|
||||
|
||||
Ok(AccessControlSearch { acp, attrs })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) unsafe fn from_raw(
|
||||
name: &str,
|
||||
uuid: Uuid,
|
||||
receiver: Uuid,
|
||||
targetscope: Filter<FilterValid>,
|
||||
attrs: &str,
|
||||
) -> Self {
|
||||
AccessControlSearch {
|
||||
acp: AccessControlProfile {
|
||||
name: name.to_string(),
|
||||
uuid,
|
||||
receiver: Some(receiver),
|
||||
targetscope,
|
||||
},
|
||||
attrs: attrs
|
||||
.split_whitespace()
|
||||
.map(|s| AttrString::from(s))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessControlDelete {
|
||||
pub acp: AccessControlProfile,
|
||||
}
|
||||
|
||||
impl AccessControlDelete {
|
||||
pub fn try_from(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
if !value.attribute_equality("class", &PVCLASS_ACD) {
|
||||
admin_error!("class access_control_delete not present.");
|
||||
return Err(OperationError::InvalidAcpState(
|
||||
"Missing access_control_delete".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(AccessControlDelete {
|
||||
acp: AccessControlProfile::try_from(qs, value)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) unsafe fn from_raw(
|
||||
name: &str,
|
||||
uuid: Uuid,
|
||||
receiver: Uuid,
|
||||
targetscope: Filter<FilterValid>,
|
||||
) -> Self {
|
||||
AccessControlDelete {
|
||||
acp: AccessControlProfile {
|
||||
name: name.to_string(),
|
||||
uuid,
|
||||
receiver: Some(receiver),
|
||||
targetscope,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessControlCreate {
|
||||
pub acp: AccessControlProfile,
|
||||
pub classes: Vec<AttrString>,
|
||||
pub attrs: Vec<AttrString>,
|
||||
}
|
||||
|
||||
impl AccessControlCreate {
|
||||
pub fn try_from(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
if !value.attribute_equality("class", &PVCLASS_ACC) {
|
||||
admin_error!("class access_control_create not present.");
|
||||
return Err(OperationError::InvalidAcpState(
|
||||
"Missing access_control_create".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let attrs = value
|
||||
.get_ava_iter_iutf8("acp_create_attr")
|
||||
.map(|i| i.map(AttrString::from).collect())
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
let classes = value
|
||||
.get_ava_iter_iutf8("acp_create_class")
|
||||
.map(|i| i.map(AttrString::from).collect())
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
Ok(AccessControlCreate {
|
||||
acp: AccessControlProfile::try_from(qs, value)?,
|
||||
classes,
|
||||
attrs,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) unsafe fn from_raw(
|
||||
name: &str,
|
||||
uuid: Uuid,
|
||||
receiver: Uuid,
|
||||
targetscope: Filter<FilterValid>,
|
||||
classes: &str,
|
||||
attrs: &str,
|
||||
) -> Self {
|
||||
AccessControlCreate {
|
||||
acp: AccessControlProfile {
|
||||
name: name.to_string(),
|
||||
uuid,
|
||||
receiver: Some(receiver),
|
||||
targetscope,
|
||||
},
|
||||
classes: classes.split_whitespace().map(AttrString::from).collect(),
|
||||
attrs: attrs.split_whitespace().map(AttrString::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessControlModify {
|
||||
pub acp: AccessControlProfile,
|
||||
pub classes: Vec<AttrString>,
|
||||
pub presattrs: Vec<AttrString>,
|
||||
pub remattrs: Vec<AttrString>,
|
||||
}
|
||||
|
||||
impl AccessControlModify {
|
||||
pub fn try_from(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
if !value.attribute_equality("class", &PVCLASS_ACM) {
|
||||
admin_error!("class access_control_modify not present.");
|
||||
return Err(OperationError::InvalidAcpState(
|
||||
"Missing access_control_modify".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let presattrs = value
|
||||
.get_ava_iter_iutf8("acp_modify_presentattr")
|
||||
.map(|i| i.map(AttrString::from).collect())
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
let remattrs = value
|
||||
.get_ava_iter_iutf8("acp_modify_removedattr")
|
||||
.map(|i| i.map(AttrString::from).collect())
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
let classes = value
|
||||
.get_ava_iter_iutf8("acp_modify_class")
|
||||
.map(|i| i.map(AttrString::from).collect())
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
Ok(AccessControlModify {
|
||||
acp: AccessControlProfile::try_from(qs, value)?,
|
||||
classes,
|
||||
presattrs,
|
||||
remattrs,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) unsafe fn from_raw(
|
||||
name: &str,
|
||||
uuid: Uuid,
|
||||
receiver: Uuid,
|
||||
targetscope: Filter<FilterValid>,
|
||||
presattrs: &str,
|
||||
remattrs: &str,
|
||||
classes: &str,
|
||||
) -> Self {
|
||||
AccessControlModify {
|
||||
acp: AccessControlProfile {
|
||||
name: name.to_string(),
|
||||
uuid,
|
||||
receiver: Some(receiver),
|
||||
targetscope,
|
||||
},
|
||||
classes: classes
|
||||
.split_whitespace()
|
||||
.map(|s| AttrString::from(s))
|
||||
.collect(),
|
||||
presattrs: presattrs
|
||||
.split_whitespace()
|
||||
.map(|s| AttrString::from(s))
|
||||
.collect(),
|
||||
remattrs: remattrs
|
||||
.split_whitespace()
|
||||
.map(|s| AttrString::from(s))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessControlProfile {
|
||||
pub name: String,
|
||||
// Currently we retrieve this but don't use it. We could depending on how we change
|
||||
// the acp update routine.
|
||||
#[allow(dead_code)]
|
||||
uuid: Uuid,
|
||||
// Must be
|
||||
// Group
|
||||
// === ⚠️ WARNING!!! ⚠️ ===
|
||||
// This is OPTION to allow migration from 10 -> 11. We have to do this because ACP is reloaded
|
||||
// so early in the boot phase that we can't have migrated the content of the receiver yet! As a
|
||||
// result we MUST be able to withstand some failure in the parse process. The INTENT is that
|
||||
// during early boot this will be None, and will NEVER match. Once started, the migration
|
||||
// will occur, and this will flip to Some. In a future version we can remove this!
|
||||
pub receiver: Option<Uuid>,
|
||||
// or
|
||||
// Filter
|
||||
// Group
|
||||
// Self
|
||||
// and
|
||||
// exclude
|
||||
// Group
|
||||
pub targetscope: Filter<FilterValid>,
|
||||
}
|
||||
|
||||
impl AccessControlProfile {
|
||||
pub(super) fn try_from(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
// Assert we have class access_control_profile
|
||||
if !value.attribute_equality("class", &PVCLASS_ACP) {
|
||||
admin_error!("class access_control_profile not present.");
|
||||
return Err(OperationError::InvalidAcpState(
|
||||
"Missing access_control_profile".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// copy name
|
||||
let name = value
|
||||
.get_ava_single_iname("name")
|
||||
.ok_or_else(|| {
|
||||
admin_error!("Missing name");
|
||||
OperationError::InvalidAcpState("Missing name".to_string())
|
||||
})?
|
||||
.to_string();
|
||||
// copy uuid
|
||||
let uuid = value.get_uuid();
|
||||
// receiver, and turn to real filter
|
||||
|
||||
// === ⚠️ WARNING!!! ⚠️ ===
|
||||
// See struct ACP for details.
|
||||
let receiver = value.get_ava_single_refer("acp_receiver_group");
|
||||
/*
|
||||
.ok_or_else(|| {
|
||||
admin_error!("Missing acp_receiver_group");
|
||||
OperationError::InvalidAcpState("Missing acp_receiver_group".to_string())
|
||||
})?;
|
||||
*/
|
||||
|
||||
// targetscope, and turn to real filter
|
||||
let targetscope_f: ProtoFilter = value
|
||||
.get_ava_single_protofilter("acp_targetscope")
|
||||
// .map(|pf| pf.clone())
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
admin_error!("Missing acp_targetscope");
|
||||
OperationError::InvalidAcpState("Missing acp_targetscope".to_string())
|
||||
})?;
|
||||
|
||||
let ident = Identity::from_internal();
|
||||
|
||||
let targetscope_i = Filter::from_rw(&ident, &targetscope_f, qs).map_err(|e| {
|
||||
admin_error!("Targetscope validation failed {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
let targetscope = targetscope_i.validate(qs.get_schema()).map_err(|e| {
|
||||
admin_error!("acp_targetscope Schema Violation {:?}", e);
|
||||
OperationError::SchemaViolation(e)
|
||||
})?;
|
||||
|
||||
Ok(AccessControlProfile {
|
||||
name,
|
||||
uuid,
|
||||
receiver,
|
||||
targetscope,
|
||||
})
|
||||
}
|
||||
}
|
104
kanidmd/lib/src/server/access/search.rs
Normal file
104
kanidmd/lib/src/server/access/search.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
use crate::prelude::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::profiles::AccessControlSearch;
|
||||
use super::AccessResult;
|
||||
use crate::filter::FilterValidResolved;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) enum SearchResult<'a> {
|
||||
Denied,
|
||||
Grant,
|
||||
Allow(BTreeSet<&'a str>),
|
||||
}
|
||||
|
||||
pub(super) fn apply_search_access<'a>(
|
||||
ident: &Identity,
|
||||
related_acp: &'a [(&AccessControlSearch, Filter<FilterValidResolved>)],
|
||||
entry: &'a Arc<EntrySealedCommitted>,
|
||||
) -> SearchResult<'a> {
|
||||
// This could be considered "slow" due to allocs each iter with the entry. We
|
||||
// could move these out of the loop and re-use, but there are likely risks to
|
||||
// that.
|
||||
let mut denied = false;
|
||||
let mut grant = false;
|
||||
let mut 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),
|
||||
};
|
||||
|
||||
// We'll add more modules later.
|
||||
|
||||
// Now finalise the decision.
|
||||
|
||||
if denied {
|
||||
SearchResult::Denied
|
||||
} else if grant {
|
||||
SearchResult::Grant
|
||||
} else {
|
||||
let allowed_attrs = if !constrain.is_empty() {
|
||||
// bit_and
|
||||
&constrain & &allow
|
||||
} else {
|
||||
allow
|
||||
};
|
||||
SearchResult::Allow(allowed_attrs)
|
||||
}
|
||||
}
|
||||
|
||||
fn search_filter_entry<'a>(
|
||||
ident: &Identity,
|
||||
related_acp: &'a [(&AccessControlSearch, Filter<FilterValidResolved>)],
|
||||
entry: &'a Arc<EntrySealedCommitted>,
|
||||
) -> AccessResult<'a> {
|
||||
// If this is an internal search, return our working set.
|
||||
match &ident.origin {
|
||||
IdentType::Internal => {
|
||||
trace!("Internal operation, bypassing access check");
|
||||
// No need to check ACS
|
||||
return AccessResult::Grant;
|
||||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_critical!("Blocking sync check");
|
||||
return AccessResult::Denied;
|
||||
}
|
||||
IdentType::User(_) => {}
|
||||
};
|
||||
info!(event = %ident, "Access check for search (filter) event");
|
||||
|
||||
match ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to search");
|
||||
return AccessResult::Denied;
|
||||
}
|
||||
AccessScope::ReadOnly | AccessScope::ReadWrite => {
|
||||
// As you were
|
||||
}
|
||||
};
|
||||
|
||||
let allowed_attrs: BTreeSet<&str> = related_acp
|
||||
.iter()
|
||||
.filter_map(|(acs, f_res)| {
|
||||
// if it applies
|
||||
if entry.entry_match_no_index(f_res) {
|
||||
security_access!(entry = ?entry.get_uuid(), acs = %acs.acp.name, "entry matches acs");
|
||||
// add search_attrs to allowed.
|
||||
Some(acs.attrs.iter().map(|s| s.as_str()))
|
||||
} else {
|
||||
// should this be `security_access`?
|
||||
trace!(entry = ?entry.get_uuid(), acs = %acs.acp.name, "entry DOES NOT match acs");
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
AccessResult::Allow(allowed_attrs)
|
||||
}
|
|
@ -14,7 +14,9 @@ use tokio::sync::{Semaphore, SemaphorePermit};
|
|||
use tracing::trace;
|
||||
|
||||
use self::access::{
|
||||
AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch,
|
||||
profiles::{
|
||||
AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch,
|
||||
},
|
||||
AccessControls, AccessControlsReadTransaction, AccessControlsTransaction,
|
||||
AccessControlsWriteTransaction,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue