From 79ff5e97750279f6cfc8fd90fcfdf3edeed3f481 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Wed, 19 Jul 2023 11:42:53 +1000 Subject: [PATCH] 1785 allow sync attr yielding via partial write admin (#1879) --- book/src/DEVELOPER_README.md | 4 +- book/src/sync/concepts.md | 29 +++++++ libs/client/src/sync_account.rs | 13 +++ server/lib/src/constants/acp.rs | 3 + server/lib/src/constants/schema.rs | 18 +++- server/lib/src/constants/uuids.rs | 2 + server/lib/src/idm/scim.rs | 63 +++++++++++++- server/lib/src/server/access/mod.rs | 110 +++++++++++++++++++++---- server/lib/src/server/access/modify.rs | 46 +++++++---- server/lib/src/server/create.rs | 6 ++ server/lib/src/server/delete.rs | 6 ++ server/lib/src/server/migrations.rs | 5 +- server/lib/src/server/mod.rs | 37 ++++++++- server/lib/src/server/modify.rs | 7 ++ tools/cli/src/cli/synch.rs | 15 ++++ tools/cli/src/opt/kanidm.rs | 12 +++ 16 files changed, 339 insertions(+), 37 deletions(-) diff --git a/book/src/DEVELOPER_README.md b/book/src/DEVELOPER_README.md index cc4fdf331..003e70a17 100644 --- a/book/src/DEVELOPER_README.md +++ b/book/src/DEVELOPER_README.md @@ -40,13 +40,15 @@ zypper in clang lld make sccache ``` You should also adjust your environment with: + ```bash export RUSTC_WRAPPER=sccache export CC="sccache /usr/bin/clang" export CXX="sccache /usr/bin/clang++" ``` -And add the following to a cargo config of your choice (such as ~/.cargo/config), adjusting for cpu arch +And add the following to a cargo config of your choice (such as ~/.cargo/config), adjusting for cpu +arch ```toml [target.aarch64-unknown-linux-gnu] diff --git a/book/src/sync/concepts.md b/book/src/sync/concepts.md index 892a67b7f..4c96a45b3 100644 --- a/book/src/sync/concepts.md +++ b/book/src/sync/concepts.md @@ -88,6 +88,35 @@ If the sync tool fails, you can investigate details in the Kanidmd server output The sync tool can run "indefinitely" if you wish for Kanidm to always import data from the external source. +## Yielding Authority of Attributes to Kanidm + +By default Kanidm assumes that authority over synchronised entries is retained by the sync tool. +This means that synchronised entries can not be written to in any capacity outside of a small number +of internal Kanidm internal attributes. + +An adminisrator may wish to allow synchronised entries to have some attributes written by the +instance locally. An example is allowing passkeys to be created on Kanidm when the external +synchronisation provider does not supply them. + +In this case the synchronisation agreement can be configured to yield it's authority over these +attributes to Kanidm. + +To configure the attributes that Kanidm can control: + +```bash +kanidm system sync set-yield-attributes [attr, ...] +kanidm system sync set-yield-attributes ipasync passkeys +``` + +This commands takes the set of attributes that should be yielded. To remove an attribute you declare +the yield set with that attribute missing. + +```bash +kanidm system sync set-yield-attributes ipasync passkeys +# To remove passkeys from being Kanidm controlled. +kanidm system sync set-yield-attributes ipasync +``` + ## Finalising the Sync Account If you are performing a migration from an external IDM to Kanidm, when that migration is completed diff --git a/libs/client/src/sync_account.rs b/libs/client/src/sync_account.rs index d7adc9939..04f556906 100644 --- a/libs/client/src/sync_account.rs +++ b/libs/client/src/sync_account.rs @@ -42,6 +42,19 @@ impl KanidmClient { .map(|values: Vec| values.get(0).cloned()) } + pub async fn idm_sync_account_set_yield_attributes( + &self, + id: &str, + attrs: &Vec, + ) -> Result<(), ClientError> { + // let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect(); + self.perform_put_request( + format!("/v1/sync_account/{}/_attr/sync_yield_authority", id).as_str(), + &attrs, + ) + .await + } + pub async fn idm_sync_account_create( &self, name: &str, diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs index 820de36f7..1ea925461 100644 --- a/server/lib/src/constants/acp.rs +++ b/server/lib/src/constants/acp.rs @@ -1606,6 +1606,7 @@ lazy_static! { ("acp_search_attr", Value::new_iutf8("jws_es256_private_key")), ("acp_search_attr", Value::new_iutf8("sync_token_session")), ("acp_search_attr", Value::new_iutf8("sync_credential_portal")), + ("acp_search_attr", Value::new_iutf8("sync_yield_authority")), ("acp_search_attr", Value::new_iutf8("sync_cookie")), ("acp_modify_removedattr", Value::new_iutf8("name")), ("acp_modify_removedattr", Value::new_iutf8("description")), @@ -1613,10 +1614,12 @@ lazy_static! { ("acp_modify_removedattr", Value::new_iutf8("sync_token_session")), ("acp_modify_removedattr", Value::new_iutf8("sync_cookie")), ("acp_modify_removedattr", Value::new_iutf8("sync_credential_portal")), + ("acp_modify_removedattr", Value::new_iutf8("sync_yield_authority")), ("acp_modify_presentattr", Value::new_iutf8("name")), ("acp_modify_presentattr", Value::new_iutf8("description")), ("acp_modify_presentattr", Value::new_iutf8("sync_token_session")), ("acp_modify_presentattr", Value::new_iutf8("sync_credential_portal")), + ("acp_modify_presentattr", Value::new_iutf8("sync_yield_authority")), ("acp_create_attr", Value::new_iutf8("class")), ("acp_create_attr", Value::new_iutf8("name")), ("acp_create_attr", Value::new_iutf8("description")), diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs index 27046556e..2e409936f 100644 --- a/server/lib/src/constants/schema.rs +++ b/server/lib/src/constants/schema.rs @@ -1477,6 +1477,21 @@ lazy_static! { ("syntax", Value::Syntax(SyntaxType::Url)), ("uuid", Value::Uuid(UUID_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL)) ); + + pub static ref E_SCHEMA_ATTR_SYNC_YIELD_AUTHORITY: EntryInitNew = entry_init!( + ("class", CLASS_OBJECT.clone()), + ("class", CLASS_SYSTEM.clone()), + ("class", CLASS_ATTRIBUTETYPE.clone()), + ( + "description", + Value::new_utf8s("A set of attributes that have their authority yielded to Kanidm in a sync agreement.") + ), + ("unique", Value::Bool(false)), + ("multivalue", Value::Bool(true)), + ("attributename", Value::new_iutf8("sync_yield_authority")), + ("syntax", Value::Syntax(SyntaxType::Utf8StringInsensitive)), + ("uuid", Value::Uuid(UUID_SCHEMA_ATTR_SYNC_YIELD_AUTHORITY)) + ); } // === classes === @@ -1708,7 +1723,8 @@ pub const JSON_SCHEMA_CLASS_SYNC_ACCOUNT: &str = r#" "systemmay": [ "sync_token_session", "sync_cookie", - "sync_credential_portal" + "sync_credential_portal", + "sync_yield_authority" ], "uuid": [ "00000000-0000-0000-0000-ffff00000114" diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs index b27c9f313..2cca82cbd 100644 --- a/server/lib/src/constants/uuids.rs +++ b/server/lib/src/constants/uuids.rs @@ -232,6 +232,8 @@ pub const UUID_SCHEMA_ATTR_NAME_HISTORY: Uuid = uuid!("00000000-0000-0000-0000-f pub const UUID_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL: Uuid = uuid!("00000000-0000-0000-0000-ffff00000136"); pub const UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC: Uuid = uuid!("00000000-0000-0000-0000-ffff00000137"); +pub const UUID_SCHEMA_ATTR_SYNC_YIELD_AUTHORITY: Uuid = + uuid!("00000000-0000-0000-0000-ffff00000138"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. diff --git a/server/lib/src/idm/scim.rs b/server/lib/src/idm/scim.rs index 185f9b11f..866681015 100644 --- a/server/lib/src/idm/scim.rs +++ b/server/lib/src/idm/scim.rs @@ -580,7 +580,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { let sync_refresh = matches!(&changes.from_state, ScimSyncState::Refresh); // Get the sync authority set from the entry. - let sync_authority_set = BTreeSet::default(); + let sync_authority_set = sync_entry + .get_ava_as_iutf8("sync_yield_authority") + .cloned() + .unwrap_or_default(); // Transform the changes into something that supports lookups. let change_entries: BTreeMap = changes @@ -2764,6 +2767,64 @@ mod tests { assert!(idms_prox_write.commit().is_ok()); } + #[idm_test] + async fn test_idm_scim_sync_yield_authority( + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed, + ) { + let ct = Duration::from_secs(TEST_CURRENT_TIME); + let mut idms_prox_write = idms.proxy_write(ct).await; + let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct); + let sse = ScimSyncUpdateEvent { ident }; + + let changes = + serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync"); + + assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok()); + + // Now we set the sync agreement to have description yielded. + assert!(idms_prox_write + .qs_write + .internal_modify_uuid( + sync_uuid, + &ModifyList::new_purge_and_set( + "sync_yield_authority", + Value::new_iutf8("legalname") + ) + ) + .is_ok()); + + let testuser_filter = filter!(f_eq("name", PartialValue::new_iname("testuser"))); + + // We then can change our user. + assert!(idms_prox_write + .qs_write + .internal_modify( + &testuser_filter, + &ModifyList::new_purge_and_set( + "legalname", + Value::Utf8("Test Userington the First".to_string()) + ) + ) + .is_ok()); + + let changes = + serde_json::from_str(TEST_SYNC_SCIM_IPA_REFRESH_1).expect("failed to parse scim sync"); + + assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok()); + + // Finally, now the gidnumber should not have changed. + let testuser = idms_prox_write + .qs_write + .internal_search(testuser_filter) + .map(|mut results| results.pop().expect("Empty result set")) + .expect("Failed to access testuser"); + + assert!(testuser.get_ava_single_utf8("legalname") == Some("Test Userington the First")); + + assert!(idms_prox_write.commit().is_ok()); + } + #[idm_test] async fn test_idm_scim_sync_finalise_1(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) { let ct = Duration::from_secs(TEST_CURRENT_TIME); diff --git a/server/lib/src/server/access/mod.rs b/server/lib/src/server/access/mod.rs index 24e0f3785..861648387 100644 --- a/server/lib/src/server/access/mod.rs +++ b/server/lib/src/server/access/mod.rs @@ -13,6 +13,7 @@ //! - the ability to turn an entry into a partial-entry for results send //! requirements (also search). +use hashbrown::HashMap; use std::cell::Cell; use std::collections::BTreeSet; use std::ops::DerefMut; @@ -90,6 +91,7 @@ struct AccessControlsInner { acps_create: Vec, acps_modify: Vec, acps_delete: Vec, + sync_agreements: HashMap>, // Oauth2 // Sync prov } @@ -106,6 +108,7 @@ pub trait AccessControlsTransaction<'a> { fn get_create(&self) -> &Vec; fn get_modify(&self) -> &Vec; fn get_delete(&self) -> &Vec; + fn get_sync_agreements(&self) -> &HashMap>; #[allow(clippy::mut_from_ref)] fn get_acp_resolve_filter_cache( @@ -447,8 +450,10 @@ pub trait AccessControlsTransaction<'a> { debug!(?requested_rem, "Requested remove set"); debug!(?requested_classes, "Requested class set"); + let sync_agmts = self.get_sync_agreements(); + let r = entries.iter().all(|e| { - match apply_modify_access(&me.ident, related_acp.as_slice(), e) { + match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) { ModifyResult::Denied => false, ModifyResult::Grant => true, ModifyResult::Allow { pres, rem, cls } => { @@ -581,7 +586,9 @@ pub trait AccessControlsTransaction<'a> { debug!(?requested_rem, "Requested remove set"); debug!(?requested_classes, "Requested class set"); - match apply_modify_access(&me.ident, related_acp.as_slice(), e) { + let sync_agmts = self.get_sync_agreements(); + + match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) { ModifyResult::Denied => false, ModifyResult::Grant => true, ModifyResult::Allow { pres, rem, cls } => { @@ -792,6 +799,8 @@ pub trait AccessControlsTransaction<'a> { let modify_related_acp = self.modify_related_acp(ident); let delete_related_acp = self.delete_related_acp(ident); + let sync_agmts = self.get_sync_agreements(); + let effective_permissions: Vec<_> = entries .iter() .map(|e| { @@ -807,17 +816,20 @@ pub trait AccessControlsTransaction<'a> { }; // == modify == - - let (modify_pres, modify_rem, modify_class) = - match apply_modify_access(ident, modify_related_acp.as_slice(), e) { - ModifyResult::Denied => (Access::Denied, Access::Denied, Access::Denied), - ModifyResult::Grant => (Access::Grant, Access::Grant, Access::Grant), - ModifyResult::Allow { pres, rem, cls } => ( - Access::Allow(pres.into_iter().map(|s| s.into()).collect()), - Access::Allow(rem.into_iter().map(|s| s.into()).collect()), - Access::Allow(cls.into_iter().map(|s| s.into()).collect()), - ), - }; + let (modify_pres, modify_rem, modify_class) = match apply_modify_access( + ident, + modify_related_acp.as_slice(), + sync_agmts, + e, + ) { + ModifyResult::Denied => (Access::Denied, Access::Denied, Access::Denied), + ModifyResult::Grant => (Access::Grant, Access::Grant, Access::Grant), + ModifyResult::Allow { pres, rem, cls } => ( + Access::Allow(pres.into_iter().map(|s| s.into()).collect()), + Access::Allow(rem.into_iter().map(|s| s.into()).collect()), + Access::Allow(cls.into_iter().map(|s| s.into()).collect()), + ), + }; // == delete == let delete = delete_related_acp.iter().any(|(acd, f_res)| { @@ -895,6 +907,13 @@ impl<'a> AccessControlsWriteTransaction<'a> { Ok(()) } + pub fn update_sync_agreements(&mut self, mut sync_agreements: HashMap>) { + std::mem::swap( + &mut sync_agreements, + &mut self.inner.deref_mut().sync_agreements, + ); + } + pub fn commit(self) -> Result<(), OperationError> { self.inner.commit(); @@ -919,6 +938,10 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsWriteTransaction<'a> { &self.inner.acps_delete } + fn get_sync_agreements(&self) -> &HashMap> { + &self.inner.sync_agreements + } + fn get_acp_resolve_filter_cache( &self, ) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter), Filter, ()> @@ -969,6 +992,10 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsReadTransaction<'a> { &self.inner.acps_delete } + fn get_sync_agreements(&self) -> &HashMap> { + &self.inner.sync_agreements + } + fn get_acp_resolve_filter_cache( &self, ) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter), Filter, ()> @@ -999,6 +1026,7 @@ impl Default for AccessControls { acps_create: Vec::new(), acps_modify: Vec::new(), acps_delete: Vec::new(), + sync_agreements: HashMap::default(), }), // Allow the expect, if this fails it represents a programming/development // failure. @@ -1036,6 +1064,7 @@ impl AccessControls { #[cfg(test)] mod tests { + use hashbrown::HashMap; use std::collections::BTreeSet; use std::sync::Arc; @@ -1794,6 +1823,33 @@ mod tests { acw.update_modify($controls).expect("Failed to update"); let acw = acw; + let res = acw + .modify_allow_operation(&mut $me, $entries) + .expect("op failed"); + + debug!("result --> {:?}", res); + debug!("expect --> {:?}", $expect); + // should be ok, and same as expect. + assert!(res == $expect); + }}; + ( + $me:expr, + $controls:expr, + $sync_uuid:expr, + $sync_yield_attr:expr, + $entries:expr, + $expect:expr + ) => {{ + let ac = AccessControls::default(); + let mut acw = ac.write(); + acw.update_modify($controls).expect("Failed to update"); + let mut sync_agmt = HashMap::new(); + let mut set = BTreeSet::new(); + set.insert($sync_yield_attr.to_string()); + sync_agmt.insert($sync_uuid, set); + acw.update_sync_agreements(sync_agmt); + let acw = acw; + let res = acw .modify_allow_operation(&mut $me, $entries) .expect("op failed"); @@ -2449,10 +2505,12 @@ mod tests { }; let r1_set = vec![Arc::new(ev1)]; + let sync_uuid = Uuid::new_v4(); let ev2 = unsafe { entry_init!( ("class", CLASS_ACCOUNT.clone()), ("class", CLASS_SYNC_OBJECT.clone()), + ("sync_parent_uuid", Value::Refer(sync_uuid)), ("name", Value::new_iname("testperson1")), ("uuid", Value::Uuid(UUID_TEST_ACCOUNT_1)) ) @@ -2555,7 +2613,31 @@ mod tests { // Test reject rem test_acp_modify!(&me_rem, vec![acp_allow.clone()], &r2_set, false); // Test reject purge - test_acp_modify!(&me_purge, vec![acp_allow], &r2_set, false); + test_acp_modify!(&me_purge, vec![acp_allow.clone()], &r2_set, false); + + // Test that when an attribute is in the sync_yield state that it can be + // modified by a user. + + // Test allow pres + test_acp_modify!( + &me_pres, + vec![acp_allow.clone()], + sync_uuid, + "name", + &r2_set, + true + ); + // Test allow rem + test_acp_modify!( + &me_rem, + vec![acp_allow.clone()], + sync_uuid, + "name", + &r2_set, + true + ); + // Test allow purge + test_acp_modify!(&me_purge, vec![acp_allow], sync_uuid, "name", &r2_set, true); } #[test] diff --git a/server/lib/src/server/access/modify.rs b/server/lib/src/server/access/modify.rs index fcbae0ba5..12e3d2c90 100644 --- a/server/lib/src/server/access/modify.rs +++ b/server/lib/src/server/access/modify.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use hashbrown::HashMap; use std::collections::BTreeSet; use super::profiles::AccessControlModify; @@ -19,7 +20,7 @@ pub(super) enum ModifyResult<'a> { pub(super) fn apply_modify_access<'a>( ident: &Identity, related_acp: &'a [(&AccessControlModify, Filter)], - // may need sync agreements later. + sync_agreements: &'a HashMap>, entry: &'a Arc, ) -> ModifyResult<'a> { let mut denied = false; @@ -46,7 +47,7 @@ pub(super) fn apply_modify_access<'a>( // Check with protected if we should proceed. // If it's a sync entry, constrain it. - match modify_sync_constrain(ident, entry) { + match modify_sync_constrain(ident, entry, sync_agreements) { AccessResult::Denied => denied = true, AccessResult::Constrain(mut set) => { constrain_rem.extend(set.iter().copied()); @@ -188,6 +189,7 @@ fn modify_cls_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessResult<' fn modify_sync_constrain<'a>( ident: &Identity, entry: &'a Arc, + sync_agreements: &'a HashMap>, ) -> AccessResult<'a> { match &ident.origin { IdentType::Internal => AccessResult::Ignore, @@ -197,22 +199,34 @@ fn modify_sync_constrain<'a>( AccessResult::Ignore } IdentType::User(_) => { - if let Some(classes) = entry.get_ava_set("class") { - // If the entry is sync object. - if classes.contains(&PVCLASS_SYNC_OBJECT) { - // Constrain to a limited set of attributes. - AccessResult::Constrain(btreeset![ - "user_auth_token_session", - "oauth2_session", - "oauth2_consent_scope_map", - "credential_update_intent_token" - ]) - } else { - AccessResult::Ignore + // We need to meet these conditions. + // * We are a sync object + // * We have a sync_parent_uuid + let is_sync = entry + .get_ava_set("class") + .map(|classes| classes.contains(&PVCLASS_SYNC_OBJECT)) + .unwrap_or(false); + + if !is_sync { + return AccessResult::Ignore; + } + + if let Some(sync_uuid) = entry.get_ava_single_refer("sync_parent_uuid") { + let mut set = btreeset![ + "user_auth_token_session", + "oauth2_session", + "oauth2_consent_scope_map", + "credential_update_intent_token" + ]; + + if let Some(sync_yield_authority) = sync_agreements.get(&sync_uuid) { + set.extend(sync_yield_authority.iter().map(|s| s.as_str())) } + + AccessResult::Constrain(set) } else { - // Nothing to check. - AccessResult::Ignore + warn!(entry = ?entry.get_uuid(), "sync_parent_uuid not found on sync object, preventing all access"); + AccessResult::Denied } } } diff --git a/server/lib/src/server/create.rs b/server/lib/src/server/create.rs index b25779d29..5fe2d6426 100644 --- a/server/lib/src/server/create.rs +++ b/server/lib/src/server/create.rs @@ -126,6 +126,11 @@ impl<'a> QueryServerWriteTransaction<'a> { .iter() .any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)); } + if !self.changed_sync_agreement { + self.changed_sync_agreement = commit_cand + .iter() + .any(|e| e.attribute_equality("class", &PVCLASS_SYNC_ACCOUNT)); + } self.changed_uuid .extend(commit_cand.iter().map(|e| e.get_uuid())); @@ -134,6 +139,7 @@ impl<'a> QueryServerWriteTransaction<'a> { acp_reload = ?self.changed_acp, oauth2_reload = ?self.changed_oauth2, domain_reload = ?self.changed_domain, + changed_sync_agreement = ?self.changed_sync_agreement, ); // We are complete, finalise logging and return diff --git a/server/lib/src/server/delete.rs b/server/lib/src/server/delete.rs index c3203bac6..ef46aa4b2 100644 --- a/server/lib/src/server/delete.rs +++ b/server/lib/src/server/delete.rs @@ -117,6 +117,11 @@ impl<'a> QueryServerWriteTransaction<'a> { .iter() .any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)); } + if !self.changed_sync_agreement { + self.changed_sync_agreement = del_cand + .iter() + .any(|e| e.attribute_equality("uuid", &PVCLASS_SYNC_ACCOUNT)); + } self.changed_uuid .extend(del_cand.iter().map(|e| e.get_uuid())); @@ -126,6 +131,7 @@ impl<'a> QueryServerWriteTransaction<'a> { acp_reload = ?self.changed_acp, oauth2_reload = ?self.changed_oauth2, domain_reload = ?self.changed_domain, + changed_sync_agreement = ?self.changed_sync_agreement ); // Send result diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index c5a5a48c3..9dc2d56c0 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -435,7 +435,10 @@ impl<'a> QueryServerWriteTransaction<'a> { pub fn initialise_schema_idm(&mut self) -> Result<(), OperationError> { admin_debug!("initialise_schema_idm -> start ..."); - let idm_schema_attrs = [E_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL.clone()]; + let idm_schema_attrs = [ + E_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL.clone(), + E_SCHEMA_ATTR_SYNC_YIELD_AUTHORITY.clone(), + ]; let r: Result<(), _> = idm_schema_attrs .into_iter() diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index 3ef906007..b147e0c28 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -6,7 +6,8 @@ use std::sync::Arc; use concread::arcache::{ARCache, ARCacheBuilder, ARCacheReadTxn}; use concread::cowcell::*; -use hashbrown::HashSet; +use hashbrown::{HashMap, HashSet}; +use std::collections::BTreeSet; use tokio::sync::{Semaphore, SemaphorePermit}; use tracing::trace; @@ -113,6 +114,7 @@ pub struct QueryServerWriteTransaction<'a> { pub(crate) changed_acp: bool, pub(crate) changed_oauth2: bool, pub(crate) changed_domain: bool, + changed_sync_agreement: bool, // Store the list of changed uuids for other invalidation needs? pub(crate) changed_uuid: HashSet, _db_ticket: SemaphorePermit<'a>, @@ -1149,6 +1151,7 @@ impl QueryServer { changed_acp: false, changed_oauth2: false, changed_domain: false, + changed_sync_agreement: false, changed_uuid: HashSet::new(), _db_ticket: db_ticket, _write_ticket: write_ticket, @@ -1172,7 +1175,7 @@ impl<'a> QueryServerWriteTransaction<'a> { &mut self.dyngroup_cache } - #[instrument(level = "debug", name = "reload_schema", skip(self))] + #[instrument(level = "debug", skip_all)] pub(crate) fn reload_schema(&mut self) -> Result<(), OperationError> { // supply entries to the writable schema to reload from. // find all attributes. @@ -1245,6 +1248,7 @@ impl<'a> QueryServerWriteTransaction<'a> { Ok(()) } + #[instrument(level = "debug", skip_all)] fn reload_accesscontrols(&mut self) -> Result<(), OperationError> { // supply entries to the writable access controls to reload from. // This has to be done in FOUR passes - one for each type! @@ -1255,6 +1259,30 @@ impl<'a> QueryServerWriteTransaction<'a> { // the entry lists themself. trace!("ACP reload started ..."); + // Update the set of sync agreements + + let filt = filter!(f_eq("class", PVCLASS_SYNC_ACCOUNT.clone())); + + let res = self.internal_search(filt).map_err(|e| { + admin_error!( + err = ?e, + "reload accesscontrols internal search failed", + ); + e + })?; + + let sync_agreement_map: HashMap> = res + .iter() + .filter_map(|e| { + e.get_ava_as_iutf8("sync_yield_authority") + .cloned() + .map(|set| (e.get_uuid(), set)) + }) + .collect(); + + self.accesscontrols + .update_sync_agreements(sync_agreement_map); + // Update search let filt = filter!(f_and!([ f_eq("class", PVCLASS_ACP.clone()), @@ -1498,7 +1526,10 @@ impl<'a> QueryServerWriteTransaction<'a> { // based on any modifications that have occurred. // IF SCHEMA CHANGED WE MUST ALSO RELOAD!!! IE if schema had an attr removed // that we rely on we MUST fail this here!! - if self.changed_schema || self.changed_acp { + // + // Also note that changing sync agreements triggers an acp reload since + // access controls need to be aware of these agreements. + if self.changed_schema || self.changed_acp || self.changed_sync_agreement { self.reload_accesscontrols()?; } else { // On a reload the cache is dropped, otherwise we tell accesscontrols diff --git a/server/lib/src/server/modify.rs b/server/lib/src/server/modify.rs index 1cbc0d34e..0e609e3a3 100644 --- a/server/lib/src/server/modify.rs +++ b/server/lib/src/server/modify.rs @@ -210,6 +210,12 @@ impl<'a> QueryServerWriteTransaction<'a> { .chain(pre_candidates.iter().map(|e| e.as_ref())) .any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)); } + if !self.changed_sync_agreement { + self.changed_sync_agreement = norm_cand + .iter() + .chain(pre_candidates.iter().map(|e| e.as_ref())) + .any(|e| e.attribute_equality("class", &PVCLASS_SYNC_ACCOUNT)); + } self.changed_uuid.extend( norm_cand @@ -223,6 +229,7 @@ impl<'a> QueryServerWriteTransaction<'a> { acp_reload = ?self.changed_acp, oauth2_reload = ?self.changed_oauth2, domain_reload = ?self.changed_domain, + changed_sync_agreement = ?self.changed_sync_agreement ); // return diff --git a/tools/cli/src/cli/synch.rs b/tools/cli/src/cli/synch.rs index 99c132ed3..89535e393 100644 --- a/tools/cli/src/cli/synch.rs +++ b/tools/cli/src/cli/synch.rs @@ -13,6 +13,7 @@ impl SynchOpt { | SynchOpt::ForceRefresh { copt, .. } | SynchOpt::Finalise { copt, .. } | SynchOpt::Terminate { copt, .. } + | SynchOpt::SetYieldAttributes { copt, .. } | SynchOpt::SetCredentialPortal { copt, .. } => copt.debug, } } @@ -83,6 +84,20 @@ impl SynchOpt { Err(e) => error!("Error -> {:?}", e), } } + SynchOpt::SetYieldAttributes { + account_id, + copt, + attrs, + } => { + let client = copt.to_client(OpType::Write).await; + match client + .idm_sync_account_set_yield_attributes(account_id, attrs) + .await + { + Ok(()) => println!("Success"), + Err(e) => error!("Error -> {:?}", e), + } + } SynchOpt::ForceRefresh { account_id, copt } => { let client = copt.to_client(OpType::Write).await; match client.idm_sync_account_force_refresh(account_id).await { diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index 039e81e55..d7e2ba113 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -856,6 +856,18 @@ pub enum SynchOpt { #[clap(flatten)] copt: CommonOpt, }, + /// Set the list of attributes that have their authority yielded from the sync account + /// and are allowed to be modified by kanidm and users. Any attributes not listed in + /// in this command will have their authority returned to the sync account. + #[clap(name = "set-yield-attributes")] + SetYieldAttributes { + #[clap()] + account_id: String, + #[clap(flatten)] + copt: CommonOpt, + #[clap(name = "attributes")] + attrs: Vec, + }, /// Reset the sync cookie of this connector, so that on the next operation of the sync tool /// a full refresh of the provider is requested. Kanidm attributes that have been granted /// authority will *not* be lost or deleted.