mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 04:27:02 +01:00
20250114 3325 SCIM access control (#3359)
Add an extended query operation to return effective access controls so that UI's can dynamically display what is or is not editable on an entry.
This commit is contained in:
parent
b03f842728
commit
b3be758b74
|
@ -1,4 +1,5 @@
|
|||
//! These are types that a client will send to the server.
|
||||
use super::ScimEntryGetQuery;
|
||||
use super::ScimOauth2ClaimMapJoinChar;
|
||||
use crate::attribute::Attribute;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -89,10 +90,17 @@ pub struct ScimEntryPutKanidm {
|
|||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ScimStrings(#[serde_as(as = "OneOrMany<_, PreferMany>")] pub Vec<String>);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct ScimEntryPutGeneric {
|
||||
// id is only used to target the entry in question
|
||||
pub id: Uuid,
|
||||
|
||||
#[serde(flatten)]
|
||||
/// Non-standard extension - allow query options to be set in a put request. This
|
||||
/// is because a put request also returns the entry state post put, so we want
|
||||
/// to allow putters to adjust and control what is returned here.
|
||||
pub query: ScimEntryGetQuery,
|
||||
|
||||
// external_id can't be set by put
|
||||
// meta is skipped on put
|
||||
// Schemas are decoded as part of "attrs".
|
||||
|
@ -119,6 +127,10 @@ impl TryFrom<ScimEntryPutKanidm> for ScimEntryPutGeneric {
|
|||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok(ScimEntryPutGeneric { id, attrs })
|
||||
Ok(ScimEntryPutGeneric {
|
||||
id,
|
||||
attrs,
|
||||
query: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ use crate::attribute::Attribute;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Not;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use serde_with::formats::CommaSeparator;
|
||||
|
@ -47,10 +48,12 @@ pub struct ScimEntryGeneric {
|
|||
/// SCIM Query Parameters used during the get of a single entry
|
||||
#[serde_as]
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct ScimEntryGetQuery {
|
||||
#[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, Attribute>>")]
|
||||
pub attributes: Option<Vec<Attribute>>,
|
||||
#[serde(default, skip_serializing_if = "<&bool>::not")]
|
||||
pub ext_access_check: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
|
@ -178,7 +181,10 @@ mod tests {
|
|||
fn scim_entry_get_query() {
|
||||
use super::*;
|
||||
|
||||
let q = ScimEntryGetQuery { attributes: None };
|
||||
let q = ScimEntryGetQuery {
|
||||
attributes: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let txt = serde_urlencoded::to_string(&q).unwrap();
|
||||
|
||||
|
@ -186,6 +192,7 @@ mod tests {
|
|||
|
||||
let q = ScimEntryGetQuery {
|
||||
attributes: Some(vec![Attribute::Name]),
|
||||
ext_access_check: false,
|
||||
};
|
||||
|
||||
let txt = serde_urlencoded::to_string(&q).unwrap();
|
||||
|
@ -193,9 +200,10 @@ mod tests {
|
|||
|
||||
let q = ScimEntryGetQuery {
|
||||
attributes: Some(vec![Attribute::Name, Attribute::Spn]),
|
||||
ext_access_check: true,
|
||||
};
|
||||
|
||||
let txt = serde_urlencoded::to_string(&q).unwrap();
|
||||
assert_eq!(txt, "attributes=name%2Cspn");
|
||||
assert_eq!(txt, "attributes=name%2Cspn&ext_access_check=true");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,14 +16,54 @@ use uuid::Uuid;
|
|||
/// A strongly typed ScimEntry that is for transmission to clients. This uses
|
||||
/// Kanidm internal strong types for values allowing direct serialisation and
|
||||
/// transmission.
|
||||
#[serde_as]
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Debug, Clone, ToSchema)]
|
||||
pub struct ScimEntryKanidm {
|
||||
#[serde(flatten)]
|
||||
pub header: ScimEntryHeader,
|
||||
|
||||
pub ext_access_check: Option<ScimEffectiveAccess>,
|
||||
#[serde(flatten)]
|
||||
pub attrs: BTreeMap<Attribute, ScimValueKanidm>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, ToSchema)]
|
||||
pub enum ScimAttributeEffectiveAccess {
|
||||
/// All attributes on the entry have this permission granted
|
||||
Grant,
|
||||
/// All attributes on the entry have this permission denied
|
||||
Denied,
|
||||
/// The following attributes on the entry have this permission granted
|
||||
Allow(BTreeSet<Attribute>),
|
||||
}
|
||||
|
||||
impl ScimAttributeEffectiveAccess {
|
||||
/// Check if the effective access allows or denies this attribute
|
||||
pub fn check(&self, attr: &Attribute) -> bool {
|
||||
match self {
|
||||
Self::Grant => true,
|
||||
Self::Denied => false,
|
||||
Self::Allow(set) => set.contains(attr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScimEffectiveAccess {
|
||||
/// The identity that inherits the effective permission
|
||||
pub ident: Uuid,
|
||||
/// If the ident may delete the target entry
|
||||
pub delete: bool,
|
||||
/// The set of effective access over search events
|
||||
pub search: ScimAttributeEffectiveAccess,
|
||||
/// The set of effective access over modify present events
|
||||
pub modify_present: ScimAttributeEffectiveAccess,
|
||||
/// The set of effective access over modify remove events
|
||||
pub modify_remove: ScimAttributeEffectiveAccess,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScimAddress {
|
||||
|
|
|
@ -20,10 +20,7 @@ fn parse_attributes(
|
|||
input: &syn::ItemFn,
|
||||
) -> Result<(proc_macro2::TokenStream, Flags), syn::Error> {
|
||||
let args: Punctuated<ExprAssign, syn::token::Comma> =
|
||||
match Punctuated::<ExprAssign, Token![,]>::parse_terminated.parse(args.clone()) {
|
||||
Ok(it) => it,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
Punctuated::<ExprAssign, Token![,]>::parse_terminated.parse(args.clone())?;
|
||||
|
||||
let args_are_allowed = args.pairs().all(|p| {
|
||||
ALLOWED_ATTRIBUTES.to_vec().contains(
|
||||
|
|
|
@ -41,12 +41,14 @@ use crate::prelude::*;
|
|||
use crate::repl::cid::Cid;
|
||||
use crate::repl::entry::EntryChangeState;
|
||||
use crate::repl::proto::{ReplEntryV1, ReplIncrementalEntryV1};
|
||||
use crate::server::access::AccessEffectivePermission;
|
||||
use compact_jwt::JwsEs256Signer;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use kanidm_proto::internal::ImageValue;
|
||||
use kanidm_proto::internal::{
|
||||
ConsistencyError, Filter as ProtoFilter, OperationError, SchemaError, UiHint,
|
||||
};
|
||||
use kanidm_proto::scim_v1::server::ScimEffectiveAccess;
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use ldap3_proto::simple::{LdapPartialAttribute, LdapSearchResultEntry};
|
||||
use openssl::ec::EcKey;
|
||||
|
@ -160,6 +162,7 @@ pub struct EntrySealed {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct EntryReduced {
|
||||
uuid: Uuid,
|
||||
effective_access: Option<Box<AccessEffectivePermission>>,
|
||||
}
|
||||
|
||||
// One day this is going to be Map<Attribute, ValueSet> - @yaleman
|
||||
|
@ -1782,6 +1785,7 @@ impl Entry<EntrySealed, EntryCommitted> {
|
|||
Entry {
|
||||
valid: EntryReduced {
|
||||
uuid: self.valid.uuid,
|
||||
effective_access: None,
|
||||
},
|
||||
state: self.state,
|
||||
attrs: self.attrs,
|
||||
|
@ -1793,6 +1797,7 @@ impl Entry<EntrySealed, EntryCommitted> {
|
|||
pub fn reduce_attributes(
|
||||
&self,
|
||||
allowed_attrs: &BTreeSet<Attribute>,
|
||||
effective_access: Option<Box<AccessEffectivePermission>>,
|
||||
) -> Entry<EntryReduced, EntryCommitted> {
|
||||
// Remove all attrs from our tree that are NOT in the allowed set.
|
||||
let f_attrs: Map<_, _> = self
|
||||
|
@ -1809,6 +1814,7 @@ impl Entry<EntrySealed, EntryCommitted> {
|
|||
|
||||
let valid = EntryReduced {
|
||||
uuid: self.valid.uuid,
|
||||
effective_access,
|
||||
};
|
||||
let state = self.state.clone();
|
||||
|
||||
|
@ -2293,6 +2299,22 @@ impl Entry<EntryReduced, EntryCommitted> {
|
|||
|
||||
let attrs = result?;
|
||||
|
||||
let ext_access_check = self.valid.effective_access.as_ref().map(|eff_acc| {
|
||||
let ident = eff_acc.ident;
|
||||
let delete = eff_acc.delete;
|
||||
let search = (&eff_acc.search).into();
|
||||
let modify_present = (&eff_acc.modify_pres).into();
|
||||
let modify_remove = (&eff_acc.modify_rem).into();
|
||||
|
||||
ScimEffectiveAccess {
|
||||
ident,
|
||||
delete,
|
||||
search,
|
||||
modify_present,
|
||||
modify_remove,
|
||||
}
|
||||
});
|
||||
|
||||
let id = self.get_uuid();
|
||||
|
||||
// Not sure how I want to handle this yet, I think we need some schema changes
|
||||
|
@ -2309,6 +2331,7 @@ impl Entry<EntryReduced, EntryCommitted> {
|
|||
// entry to store some extra metadata.
|
||||
meta: None,
|
||||
},
|
||||
ext_access_check,
|
||||
attrs,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ pub struct SearchEvent {
|
|||
// This is the original filter, for the purpose of ACI checking.
|
||||
pub filter_orig: Filter<FilterValid>,
|
||||
pub attrs: Option<BTreeSet<Attribute>>,
|
||||
pub effective_access_check: bool,
|
||||
}
|
||||
|
||||
impl SearchEvent {
|
||||
|
@ -99,6 +100,7 @@ impl SearchEvent {
|
|||
// We can't get this from the SearchMessage because it's annoying with the
|
||||
// current macro design.
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -132,6 +134,7 @@ impl SearchEvent {
|
|||
filter,
|
||||
filter_orig,
|
||||
attrs: r_attrs,
|
||||
effective_access_check: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -168,6 +171,7 @@ impl SearchEvent {
|
|||
filter,
|
||||
filter_orig,
|
||||
attrs: r_attrs,
|
||||
effective_access_check: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -185,6 +189,7 @@ impl SearchEvent {
|
|||
filter,
|
||||
filter_orig,
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -202,6 +207,7 @@ impl SearchEvent {
|
|||
filter,
|
||||
filter_orig,
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -217,6 +223,7 @@ impl SearchEvent {
|
|||
filter: filter.clone().into_valid(),
|
||||
filter_orig: filter.into_valid(),
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,6 +236,7 @@ impl SearchEvent {
|
|||
filter: filter.clone().into_valid(),
|
||||
filter_orig: filter.into_valid(),
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,6 +250,7 @@ impl SearchEvent {
|
|||
filter,
|
||||
filter_orig,
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,6 +269,7 @@ impl SearchEvent {
|
|||
filter,
|
||||
filter_orig,
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,6 +286,7 @@ impl SearchEvent {
|
|||
filter: filter.clone().into_valid().into_ignore_hidden(),
|
||||
filter_orig: filter.into_valid(),
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -296,6 +307,7 @@ impl SearchEvent {
|
|||
filter,
|
||||
filter_orig,
|
||||
attrs,
|
||||
effective_access_check: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -308,6 +320,7 @@ impl SearchEvent {
|
|||
filter: filter.clone().into_valid(),
|
||||
filter_orig: filter.into_valid(),
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,6 +330,7 @@ impl SearchEvent {
|
|||
filter: filter.clone(),
|
||||
filter_orig: filter,
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ use concread::arcache::ARCacheBuilder;
|
|||
use concread::cowcell::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::entry::{Entry, EntryCommitted, EntryInit, EntryNew, EntryReduced};
|
||||
use crate::entry::{Entry, EntryInit, EntryNew};
|
||||
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, SearchEvent};
|
||||
use crate::filter::{Filter, FilterValid, ResolveFilterCache, ResolveFilterCacheReadTxn};
|
||||
use crate::modify::Modify;
|
||||
|
@ -36,6 +36,8 @@ use self::profiles::{
|
|||
AccessControlSearchResolved, AccessControlTarget, AccessControlTargetCondition,
|
||||
};
|
||||
|
||||
use kanidm_proto::scim_v1::server::ScimAttributeEffectiveAccess;
|
||||
|
||||
use self::create::{apply_create_access, CreateResult};
|
||||
use self::delete::{apply_delete_access, DeleteResult};
|
||||
use self::modify::{apply_modify_access, ModifyResult};
|
||||
|
@ -57,6 +59,16 @@ pub enum Access {
|
|||
Allow(BTreeSet<Attribute>),
|
||||
}
|
||||
|
||||
impl From<&Access> for ScimAttributeEffectiveAccess {
|
||||
fn from(value: &Access) -> Self {
|
||||
match value {
|
||||
Access::Grant => Self::Grant,
|
||||
Access::Denied => Self::Denied,
|
||||
Access::Allow(set) => Self::Allow(set.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AccessClass {
|
||||
Grant,
|
||||
|
@ -66,8 +78,9 @@ pub enum AccessClass {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AccessEffectivePermission {
|
||||
// I don't think we need this? The ident is implied by the requester.
|
||||
// ident: Uuid,
|
||||
/// Who the access applies to
|
||||
pub ident: Uuid,
|
||||
/// The target the access affects
|
||||
pub target: Uuid,
|
||||
pub delete: bool,
|
||||
pub search: Access,
|
||||
|
@ -79,12 +92,13 @@ pub struct AccessEffectivePermission {
|
|||
pub enum AccessResult {
|
||||
// Deny this operation unconditionally.
|
||||
Denied,
|
||||
// Unbounded allow, provided no denied exists.
|
||||
// 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.
|
||||
// allow anything, it constrains what might be allowed
|
||||
// by a later module.
|
||||
Constrain(BTreeSet<Attribute>),
|
||||
// Allow these attributes within constraints.
|
||||
Allow(BTreeSet<Attribute>),
|
||||
|
@ -181,7 +195,11 @@ pub trait AccessControlsTransaction<'a> {
|
|||
fn get_acp_resolve_filter_cache(&self) -> &mut ResolveFilterCacheReadTxn<'a>;
|
||||
|
||||
#[instrument(level = "trace", name = "access::search_related_acp", skip_all)]
|
||||
fn search_related_acp<'b>(&'b self, ident: &Identity) -> Vec<AccessControlSearchResolved<'b>> {
|
||||
fn search_related_acp<'b>(
|
||||
&'b self,
|
||||
ident: &Identity,
|
||||
attrs: Option<&BTreeSet<Attribute>>,
|
||||
) -> Vec<AccessControlSearchResolved<'b>> {
|
||||
let search_state = self.get_search();
|
||||
let acp_resolve_filter_cache = self.get_acp_resolve_filter_cache();
|
||||
|
||||
|
@ -249,8 +267,18 @@ pub trait AccessControlsTransaction<'a> {
|
|||
})
|
||||
.collect();
|
||||
|
||||
// Trim any search rule that doesn't provide attributes related to the request.
|
||||
let related_acp = if let Some(r_attrs) = attrs.as_ref() {
|
||||
related_acp
|
||||
.into_iter()
|
||||
.filter(|acs| !acs.acp.attrs.is_disjoint(r_attrs))
|
||||
.collect()
|
||||
} else {
|
||||
// None here means all attrs requested.
|
||||
related_acp
|
||||
};
|
||||
|
||||
related_acp
|
||||
// }
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "access::filter_entries", skip_all)]
|
||||
|
@ -267,7 +295,7 @@ pub trait AccessControlsTransaction<'a> {
|
|||
let requested_attrs: BTreeSet<Attribute> = filter_orig.get_attr_set();
|
||||
|
||||
// First get the set of acps that apply to this receiver
|
||||
let related_acp = self.search_related_acp(ident);
|
||||
let related_acp = self.search_related_acp(ident, None);
|
||||
|
||||
// For each entry.
|
||||
let entries_is_empty = entries.is_empty();
|
||||
|
@ -318,33 +346,61 @@ pub trait AccessControlsTransaction<'a> {
|
|||
name = "access::search_filter_entry_attributes",
|
||||
skip_all
|
||||
)]
|
||||
fn search_filter_entry_attributes(
|
||||
&self,
|
||||
fn search_filter_entry_attributes<'b>(
|
||||
&'b self,
|
||||
se: &SearchEvent,
|
||||
entries: Vec<Arc<EntrySealedCommitted>>,
|
||||
) -> Result<Vec<Entry<EntryReduced, EntryCommitted>>, OperationError> {
|
||||
) -> Result<Vec<EntryReducedCommitted>, OperationError> {
|
||||
struct DoEffectiveCheck<'b> {
|
||||
modify_related_acp: Vec<AccessControlModifyResolved<'b>>,
|
||||
delete_related_acp: Vec<AccessControlDeleteResolved<'b>>,
|
||||
sync_agmts: &'b HashMap<Uuid, BTreeSet<Attribute>>,
|
||||
}
|
||||
|
||||
let ident_uuid = match &se.ident.origin {
|
||||
IdentType::Internal => {
|
||||
// In production we can't risk leaking data here, so we return
|
||||
// empty sets.
|
||||
security_critical!("IMPOSSIBLE STATE: Internal search in external interface?! Returning empty for safety.");
|
||||
// No need to check ACS
|
||||
return Err(OperationError::InvalidState);
|
||||
}
|
||||
IdentType::Synch(_) => {
|
||||
security_critical!("Blocking sync check");
|
||||
return Err(OperationError::InvalidState);
|
||||
}
|
||||
IdentType::User(u) => u.entry.get_uuid(),
|
||||
};
|
||||
|
||||
// Build a reference set from the req_attrs. This is what we test against
|
||||
// to see if the attribute is something we currently want.
|
||||
|
||||
let do_effective_check = se.effective_access_check.then(|| {
|
||||
debug!("effective permission check requested during reduction phase");
|
||||
|
||||
// == modify ==
|
||||
let modify_related_acp = self.modify_related_acp(&se.ident);
|
||||
// == delete ==
|
||||
let delete_related_acp = self.delete_related_acp(&se.ident);
|
||||
|
||||
let sync_agmts = self.get_sync_agreements();
|
||||
|
||||
DoEffectiveCheck {
|
||||
modify_related_acp,
|
||||
delete_related_acp,
|
||||
sync_agmts,
|
||||
}
|
||||
});
|
||||
|
||||
// Get the relevant acps for this receiver.
|
||||
let related_acp = self.search_related_acp(&se.ident);
|
||||
let related_acp: Vec<_> = if let Some(r_attrs) = se.attrs.as_ref() {
|
||||
// If the acp doesn't overlap with our requested attrs, there is no point in
|
||||
// testing it!
|
||||
related_acp
|
||||
.into_iter()
|
||||
.filter(|acs| !acs.acp.attrs.is_disjoint(r_attrs))
|
||||
.collect()
|
||||
} else {
|
||||
related_acp
|
||||
};
|
||||
let search_related_acp = self.search_related_acp(&se.ident, se.attrs.as_ref());
|
||||
|
||||
// For each entry.
|
||||
let entries_is_empty = entries.is_empty();
|
||||
let allowed_entries: Vec<_> = entries
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
match apply_search_access(&se.ident, related_acp.as_slice(), &e) {
|
||||
.filter_map(|entry| {
|
||||
match apply_search_access(&se.ident, &search_related_acp, &entry) {
|
||||
SearchResult::Denied => {
|
||||
None
|
||||
}
|
||||
|
@ -369,11 +425,20 @@ pub trait AccessControlsTransaction<'a> {
|
|||
allowed_attrs
|
||||
};
|
||||
|
||||
if reduced_attrs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(e.reduce_attributes(&reduced_attrs))
|
||||
}
|
||||
let effective_permissions = do_effective_check.as_ref().map(|do_check| {
|
||||
self.entry_effective_permission_check(
|
||||
&se.ident,
|
||||
ident_uuid,
|
||||
&entry,
|
||||
&search_related_acp,
|
||||
&do_check.modify_related_acp,
|
||||
&do_check.delete_related_acp,
|
||||
do_check.sync_agmts,
|
||||
)
|
||||
})
|
||||
.map(Box::new);
|
||||
|
||||
Some(entry.reduce_attributes(&reduced_attrs, effective_permissions))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -798,7 +863,7 @@ pub trait AccessControlsTransaction<'a> {
|
|||
// have an entry template. I think james was right about the create being
|
||||
// a template copy op ...
|
||||
|
||||
match &ident.origin {
|
||||
let ident_uuid = match &ident.origin {
|
||||
IdentType::Internal => {
|
||||
// In production we can't risk leaking data here, so we return
|
||||
// empty sets.
|
||||
|
@ -810,7 +875,7 @@ pub trait AccessControlsTransaction<'a> {
|
|||
security_critical!("Blocking sync check");
|
||||
return Err(OperationError::InvalidState);
|
||||
}
|
||||
IdentType::User(_) => {}
|
||||
IdentType::User(u) => u.entry.get_uuid(),
|
||||
};
|
||||
|
||||
trace!(ident = %ident, "Effective permission check");
|
||||
|
@ -818,31 +883,48 @@ pub trait AccessControlsTransaction<'a> {
|
|||
|
||||
// == search ==
|
||||
// Get the relevant acps for this receiver.
|
||||
let search_related_acp = self.search_related_acp(ident);
|
||||
// Trim any search rule that doesn't provide attributes related to the request.
|
||||
let search_related_acp = if let Some(r_attrs) = attrs.as_ref() {
|
||||
search_related_acp
|
||||
.into_iter()
|
||||
.filter(|acs| !acs.acp.attrs.is_disjoint(r_attrs))
|
||||
.collect()
|
||||
} else {
|
||||
// None here means all attrs requested.
|
||||
search_related_acp
|
||||
};
|
||||
|
||||
let search_related_acp = self.search_related_acp(ident, attrs.as_ref());
|
||||
// == modify ==
|
||||
|
||||
let modify_related_acp = self.modify_related_acp(ident);
|
||||
// == delete ==
|
||||
let delete_related_acp = self.delete_related_acp(ident);
|
||||
|
||||
let sync_agmts = self.get_sync_agreements();
|
||||
|
||||
let effective_permissions: Vec<_> = entries
|
||||
.iter()
|
||||
.map(|e| {
|
||||
.map(|entry| {
|
||||
self.entry_effective_permission_check(
|
||||
ident,
|
||||
ident_uuid,
|
||||
entry,
|
||||
&search_related_acp,
|
||||
&modify_related_acp,
|
||||
&delete_related_acp,
|
||||
sync_agmts,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
effective_permissions.iter().for_each(|ep| {
|
||||
trace!(?ep);
|
||||
});
|
||||
|
||||
Ok(effective_permissions)
|
||||
}
|
||||
|
||||
fn entry_effective_permission_check<'b>(
|
||||
&'b self,
|
||||
ident: &Identity,
|
||||
ident_uuid: Uuid,
|
||||
entry: &Arc<EntrySealedCommitted>,
|
||||
search_related_acp: &[AccessControlSearchResolved<'b>],
|
||||
modify_related_acp: &[AccessControlModifyResolved<'b>],
|
||||
delete_related_acp: &[AccessControlDeleteResolved<'b>],
|
||||
sync_agmts: &HashMap<Uuid, BTreeSet<Attribute>>,
|
||||
) -> AccessEffectivePermission {
|
||||
// == search ==
|
||||
let search_effective =
|
||||
match apply_search_access(ident, search_related_acp.as_slice(), e) {
|
||||
let search_effective = match apply_search_access(ident, search_related_acp, entry) {
|
||||
SearchResult::Denied => Access::Denied,
|
||||
SearchResult::Grant => Access::Grant,
|
||||
SearchResult::Allow(allowed_attrs) => {
|
||||
|
@ -852,12 +934,8 @@ pub trait AccessControlsTransaction<'a> {
|
|||
};
|
||||
|
||||
// == modify ==
|
||||
let (modify_pres, modify_rem, modify_class) = match apply_modify_access(
|
||||
ident,
|
||||
modify_related_acp.as_slice(),
|
||||
sync_agmts,
|
||||
e,
|
||||
) {
|
||||
let (modify_pres, modify_rem, modify_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 } => (
|
||||
|
@ -868,7 +946,7 @@ pub trait AccessControlsTransaction<'a> {
|
|||
};
|
||||
|
||||
// == delete ==
|
||||
let delete_status = apply_delete_access(ident, delete_related_acp.as_slice(), e);
|
||||
let delete_status = apply_delete_access(ident, delete_related_acp, entry);
|
||||
|
||||
let delete = match delete_status {
|
||||
DeleteResult::Denied => false,
|
||||
|
@ -876,21 +954,14 @@ pub trait AccessControlsTransaction<'a> {
|
|||
};
|
||||
|
||||
AccessEffectivePermission {
|
||||
target: e.get_uuid(),
|
||||
ident: ident_uuid,
|
||||
target: entry.get_uuid(),
|
||||
delete,
|
||||
search: search_effective,
|
||||
modify_pres,
|
||||
modify_rem,
|
||||
modify_class,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
effective_permissions.iter().for_each(|ep| {
|
||||
trace!(?ep);
|
||||
});
|
||||
|
||||
Ok(effective_permissions)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2535,6 +2606,7 @@ mod tests {
|
|||
vec![],
|
||||
&r_set,
|
||||
vec![AccessEffectivePermission {
|
||||
ident: UUID_TEST_ACCOUNT_1,
|
||||
delete: false,
|
||||
target: uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
|
||||
search: Access::Allow(btreeset![Attribute::Name]),
|
||||
|
@ -2576,6 +2648,7 @@ mod tests {
|
|||
)],
|
||||
&r_set,
|
||||
vec![AccessEffectivePermission {
|
||||
ident: UUID_TEST_ACCOUNT_1,
|
||||
delete: false,
|
||||
target: uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
|
||||
search: Access::Allow(BTreeSet::new()),
|
||||
|
|
|
@ -284,7 +284,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
fn search_ext(
|
||||
&mut self,
|
||||
se: &SearchEvent,
|
||||
) -> Result<Vec<Entry<EntryReduced, EntryCommitted>>, OperationError> {
|
||||
) -> Result<Vec<EntryReducedCommitted>, OperationError> {
|
||||
/*
|
||||
* This just wraps search, but it's for the external interface
|
||||
* so as a result it also reduces the entry set's attributes at
|
||||
|
@ -1546,6 +1546,7 @@ impl QueryServerReadTransaction<'_> {
|
|||
filter: f_valid,
|
||||
filter_orig: f_intent_valid,
|
||||
attrs: r_attrs,
|
||||
effective_access_check: query.ext_access_check,
|
||||
};
|
||||
|
||||
let mut vs = self.search_ext(&se)?;
|
||||
|
@ -2639,6 +2640,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
mod tests {
|
||||
use crate::prelude::*;
|
||||
use kanidm_proto::scim_v1::server::ScimReference;
|
||||
use kanidm_proto::scim_v1::ScimEntryGetQuery;
|
||||
|
||||
#[qs_test]
|
||||
async fn test_name_to_uuid(server: &QueryServer) {
|
||||
|
@ -3046,4 +3048,47 @@ mod tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[qs_test]
|
||||
async fn test_scim_effective_access_query(server: &QueryServer) {
|
||||
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
|
||||
|
||||
let group_uuid = Uuid::new_v4();
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Group.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testgroup")),
|
||||
(Attribute::Uuid, Value::Uuid(group_uuid))
|
||||
);
|
||||
|
||||
assert!(server_txn.internal_create(vec![e1]).is_ok());
|
||||
assert!(server_txn.commit().is_ok());
|
||||
|
||||
// Now read that entry.
|
||||
|
||||
let mut server_txn = server.read().await.unwrap();
|
||||
|
||||
let idm_admin_entry = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
|
||||
let idm_admin_ident = Identity::from_impersonate_entry_readwrite(idm_admin_entry);
|
||||
|
||||
let query = ScimEntryGetQuery {
|
||||
ext_access_check: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let scim_entry = server_txn
|
||||
.scim_entry_id_get_ext(group_uuid, EntryClass::Group, query, idm_admin_ident)
|
||||
.unwrap();
|
||||
|
||||
let ext_access_check = scim_entry.ext_access_check.unwrap();
|
||||
|
||||
trace!(?ext_access_check);
|
||||
|
||||
assert!(ext_access_check.delete);
|
||||
assert!(ext_access_check.search.check(&Attribute::DirectMemberOf));
|
||||
assert!(ext_access_check.search.check(&Attribute::MemberOf));
|
||||
assert!(ext_access_check.search.check(&Attribute::Name));
|
||||
assert!(ext_access_check.modify_present.check(&Attribute::Name));
|
||||
assert!(ext_access_check.modify_remove.check(&Attribute::Name));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ pub struct ScimEntryPutEvent {
|
|||
/// Update an attribute to contain the following value state.
|
||||
/// If the attribute is None, it is removed.
|
||||
pub attrs: BTreeMap<Attribute, Option<ValueSet>>,
|
||||
|
||||
/// If an effective access check should be carried out post modification
|
||||
/// of the entries
|
||||
pub effective_access_check: bool,
|
||||
}
|
||||
|
||||
impl ScimEntryPutEvent {
|
||||
|
@ -33,10 +37,13 @@ impl ScimEntryPutEvent {
|
|||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let query = entry.query;
|
||||
|
||||
Ok(ScimEntryPutEvent {
|
||||
ident,
|
||||
target,
|
||||
attrs,
|
||||
effective_access_check: query.ext_access_check,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +61,7 @@ impl QueryServerWriteTransaction<'_> {
|
|||
ident,
|
||||
target,
|
||||
attrs,
|
||||
effective_access_check,
|
||||
} = scim_entry_put;
|
||||
|
||||
// This function transforms the put event into a modify event.
|
||||
|
@ -91,6 +99,7 @@ impl QueryServerWriteTransaction<'_> {
|
|||
filter_orig: f_intent_valid,
|
||||
// Return all attributes, even ones we didn't affect
|
||||
attrs: None,
|
||||
effective_access_check,
|
||||
};
|
||||
|
||||
let mut vs = self.search_ext(&se)?;
|
||||
|
|
|
@ -202,6 +202,7 @@ async fn test_scim_sync_entry_get(rsclient: KanidmClient) {
|
|||
// Limit the attributes we want.
|
||||
let query = ScimEntryGetQuery {
|
||||
attributes: Some(vec![Attribute::Name]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let scim_entry = rsclient
|
||||
|
@ -238,6 +239,7 @@ async fn test_scim_sync_entry_get(rsclient: KanidmClient) {
|
|||
// Limit the attributes we want.
|
||||
let query = ScimEntryGetQuery {
|
||||
attributes: Some(vec![Attribute::Name]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let scim_entry = rsclient
|
||||
|
|
|
@ -238,6 +238,7 @@ impl PersonOpt {
|
|||
aopt.aopts.account_id.as_str(),
|
||||
Some(ScimEntryGetQuery {
|
||||
attributes: Some(vec![Attribute::SshPublicKey]),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await
|
||||
|
|
Loading…
Reference in a new issue