1785 allow sync attr yielding via partial write admin (#1879)

This commit is contained in:
Firstyear 2023-07-19 11:42:53 +10:00 committed by GitHub
parent 578e064b10
commit 79ff5e9775
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 339 additions and 37 deletions

View file

@ -40,13 +40,15 @@ zypper in clang lld make sccache
``` ```
You should also adjust your environment with: You should also adjust your environment with:
```bash ```bash
export RUSTC_WRAPPER=sccache export RUSTC_WRAPPER=sccache
export CC="sccache /usr/bin/clang" export CC="sccache /usr/bin/clang"
export CXX="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 ```toml
[target.aarch64-unknown-linux-gnu] [target.aarch64-unknown-linux-gnu]

View file

@ -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 The sync tool can run "indefinitely" if you wish for Kanidm to always import data from the external
source. 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 <sync account name> [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 ## Finalising the Sync Account
If you are performing a migration from an external IDM to Kanidm, when that migration is completed If you are performing a migration from an external IDM to Kanidm, when that migration is completed

View file

@ -42,6 +42,19 @@ impl KanidmClient {
.map(|values: Vec<Url>| values.get(0).cloned()) .map(|values: Vec<Url>| values.get(0).cloned())
} }
pub async fn idm_sync_account_set_yield_attributes(
&self,
id: &str,
attrs: &Vec<String>,
) -> 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( pub async fn idm_sync_account_create(
&self, &self,
name: &str, name: &str,

View file

@ -1606,6 +1606,7 @@ lazy_static! {
("acp_search_attr", Value::new_iutf8("jws_es256_private_key")), ("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_token_session")),
("acp_search_attr", Value::new_iutf8("sync_credential_portal")), ("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_search_attr", Value::new_iutf8("sync_cookie")),
("acp_modify_removedattr", Value::new_iutf8("name")), ("acp_modify_removedattr", Value::new_iutf8("name")),
("acp_modify_removedattr", Value::new_iutf8("description")), ("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_token_session")),
("acp_modify_removedattr", Value::new_iutf8("sync_cookie")), ("acp_modify_removedattr", Value::new_iutf8("sync_cookie")),
("acp_modify_removedattr", Value::new_iutf8("sync_credential_portal")), ("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("name")),
("acp_modify_presentattr", Value::new_iutf8("description")), ("acp_modify_presentattr", Value::new_iutf8("description")),
("acp_modify_presentattr", Value::new_iutf8("sync_token_session")), ("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_credential_portal")),
("acp_modify_presentattr", Value::new_iutf8("sync_yield_authority")),
("acp_create_attr", Value::new_iutf8("class")), ("acp_create_attr", Value::new_iutf8("class")),
("acp_create_attr", Value::new_iutf8("name")), ("acp_create_attr", Value::new_iutf8("name")),
("acp_create_attr", Value::new_iutf8("description")), ("acp_create_attr", Value::new_iutf8("description")),

View file

@ -1477,6 +1477,21 @@ lazy_static! {
("syntax", Value::Syntax(SyntaxType::Url)), ("syntax", Value::Syntax(SyntaxType::Url)),
("uuid", Value::Uuid(UUID_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL)) ("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 === // === classes ===
@ -1708,7 +1723,8 @@ pub const JSON_SCHEMA_CLASS_SYNC_ACCOUNT: &str = r#"
"systemmay": [ "systemmay": [
"sync_token_session", "sync_token_session",
"sync_cookie", "sync_cookie",
"sync_credential_portal" "sync_credential_portal",
"sync_yield_authority"
], ],
"uuid": [ "uuid": [
"00000000-0000-0000-0000-ffff00000114" "00000000-0000-0000-0000-ffff00000114"

View file

@ -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 = pub const UUID_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000136"); 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_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 // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -580,7 +580,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
let sync_refresh = matches!(&changes.from_state, ScimSyncState::Refresh); let sync_refresh = matches!(&changes.from_state, ScimSyncState::Refresh);
// Get the sync authority set from the entry. // 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. // Transform the changes into something that supports lookups.
let change_entries: BTreeMap<Uuid, &ScimEntry> = changes let change_entries: BTreeMap<Uuid, &ScimEntry> = changes
@ -2764,6 +2767,64 @@ mod tests {
assert!(idms_prox_write.commit().is_ok()); 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] #[idm_test]
async fn test_idm_scim_sync_finalise_1(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) { async fn test_idm_scim_sync_finalise_1(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);

View file

@ -13,6 +13,7 @@
//! - the ability to turn an entry into a partial-entry for results send //! - the ability to turn an entry into a partial-entry for results send
//! requirements (also search). //! requirements (also search).
use hashbrown::HashMap;
use std::cell::Cell; use std::cell::Cell;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::ops::DerefMut; use std::ops::DerefMut;
@ -90,6 +91,7 @@ struct AccessControlsInner {
acps_create: Vec<AccessControlCreate>, acps_create: Vec<AccessControlCreate>,
acps_modify: Vec<AccessControlModify>, acps_modify: Vec<AccessControlModify>,
acps_delete: Vec<AccessControlDelete>, acps_delete: Vec<AccessControlDelete>,
sync_agreements: HashMap<Uuid, BTreeSet<String>>,
// Oauth2 // Oauth2
// Sync prov // Sync prov
} }
@ -106,6 +108,7 @@ pub trait AccessControlsTransaction<'a> {
fn get_create(&self) -> &Vec<AccessControlCreate>; fn get_create(&self) -> &Vec<AccessControlCreate>;
fn get_modify(&self) -> &Vec<AccessControlModify>; fn get_modify(&self) -> &Vec<AccessControlModify>;
fn get_delete(&self) -> &Vec<AccessControlDelete>; fn get_delete(&self) -> &Vec<AccessControlDelete>;
fn get_sync_agreements(&self) -> &HashMap<Uuid, BTreeSet<String>>;
#[allow(clippy::mut_from_ref)] #[allow(clippy::mut_from_ref)]
fn get_acp_resolve_filter_cache( fn get_acp_resolve_filter_cache(
@ -447,8 +450,10 @@ pub trait AccessControlsTransaction<'a> {
debug!(?requested_rem, "Requested remove set"); debug!(?requested_rem, "Requested remove set");
debug!(?requested_classes, "Requested class set"); debug!(?requested_classes, "Requested class set");
let sync_agmts = self.get_sync_agreements();
let r = entries.iter().all(|e| { 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::Denied => false,
ModifyResult::Grant => true, ModifyResult::Grant => true,
ModifyResult::Allow { pres, rem, cls } => { ModifyResult::Allow { pres, rem, cls } => {
@ -581,7 +586,9 @@ pub trait AccessControlsTransaction<'a> {
debug!(?requested_rem, "Requested remove set"); debug!(?requested_rem, "Requested remove set");
debug!(?requested_classes, "Requested class 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::Denied => false,
ModifyResult::Grant => true, ModifyResult::Grant => true,
ModifyResult::Allow { pres, rem, cls } => { ModifyResult::Allow { pres, rem, cls } => {
@ -792,6 +799,8 @@ pub trait AccessControlsTransaction<'a> {
let modify_related_acp = self.modify_related_acp(ident); let modify_related_acp = self.modify_related_acp(ident);
let delete_related_acp = self.delete_related_acp(ident); let delete_related_acp = self.delete_related_acp(ident);
let sync_agmts = self.get_sync_agreements();
let effective_permissions: Vec<_> = entries let effective_permissions: Vec<_> = entries
.iter() .iter()
.map(|e| { .map(|e| {
@ -807,17 +816,20 @@ pub trait AccessControlsTransaction<'a> {
}; };
// == modify == // == modify ==
let (modify_pres, modify_rem, modify_class) = match apply_modify_access(
let (modify_pres, modify_rem, modify_class) = ident,
match apply_modify_access(ident, modify_related_acp.as_slice(), e) { modify_related_acp.as_slice(),
ModifyResult::Denied => (Access::Denied, Access::Denied, Access::Denied), sync_agmts,
ModifyResult::Grant => (Access::Grant, Access::Grant, Access::Grant), e,
ModifyResult::Allow { pres, rem, cls } => ( ) {
Access::Allow(pres.into_iter().map(|s| s.into()).collect()), ModifyResult::Denied => (Access::Denied, Access::Denied, Access::Denied),
Access::Allow(rem.into_iter().map(|s| s.into()).collect()), ModifyResult::Grant => (Access::Grant, Access::Grant, Access::Grant),
Access::Allow(cls.into_iter().map(|s| s.into()).collect()), 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 == // == delete ==
let delete = delete_related_acp.iter().any(|(acd, f_res)| { let delete = delete_related_acp.iter().any(|(acd, f_res)| {
@ -895,6 +907,13 @@ impl<'a> AccessControlsWriteTransaction<'a> {
Ok(()) Ok(())
} }
pub fn update_sync_agreements(&mut self, mut sync_agreements: HashMap<Uuid, BTreeSet<String>>) {
std::mem::swap(
&mut sync_agreements,
&mut self.inner.deref_mut().sync_agreements,
);
}
pub fn commit(self) -> Result<(), OperationError> { pub fn commit(self) -> Result<(), OperationError> {
self.inner.commit(); self.inner.commit();
@ -919,6 +938,10 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsWriteTransaction<'a> {
&self.inner.acps_delete &self.inner.acps_delete
} }
fn get_sync_agreements(&self) -> &HashMap<Uuid, BTreeSet<String>> {
&self.inner.sync_agreements
}
fn get_acp_resolve_filter_cache( fn get_acp_resolve_filter_cache(
&self, &self,
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()> ) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
@ -969,6 +992,10 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsReadTransaction<'a> {
&self.inner.acps_delete &self.inner.acps_delete
} }
fn get_sync_agreements(&self) -> &HashMap<Uuid, BTreeSet<String>> {
&self.inner.sync_agreements
}
fn get_acp_resolve_filter_cache( fn get_acp_resolve_filter_cache(
&self, &self,
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()> ) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
@ -999,6 +1026,7 @@ impl Default for AccessControls {
acps_create: Vec::new(), acps_create: Vec::new(),
acps_modify: Vec::new(), acps_modify: Vec::new(),
acps_delete: Vec::new(), acps_delete: Vec::new(),
sync_agreements: HashMap::default(),
}), }),
// Allow the expect, if this fails it represents a programming/development // Allow the expect, if this fails it represents a programming/development
// failure. // failure.
@ -1036,6 +1064,7 @@ impl AccessControls {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use hashbrown::HashMap;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::sync::Arc; use std::sync::Arc;
@ -1794,6 +1823,33 @@ mod tests {
acw.update_modify($controls).expect("Failed to update"); acw.update_modify($controls).expect("Failed to update");
let acw = acw; 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 let res = acw
.modify_allow_operation(&mut $me, $entries) .modify_allow_operation(&mut $me, $entries)
.expect("op failed"); .expect("op failed");
@ -2449,10 +2505,12 @@ mod tests {
}; };
let r1_set = vec![Arc::new(ev1)]; let r1_set = vec![Arc::new(ev1)];
let sync_uuid = Uuid::new_v4();
let ev2 = unsafe { let ev2 = unsafe {
entry_init!( entry_init!(
("class", CLASS_ACCOUNT.clone()), ("class", CLASS_ACCOUNT.clone()),
("class", CLASS_SYNC_OBJECT.clone()), ("class", CLASS_SYNC_OBJECT.clone()),
("sync_parent_uuid", Value::Refer(sync_uuid)),
("name", Value::new_iname("testperson1")), ("name", Value::new_iname("testperson1")),
("uuid", Value::Uuid(UUID_TEST_ACCOUNT_1)) ("uuid", Value::Uuid(UUID_TEST_ACCOUNT_1))
) )
@ -2555,7 +2613,31 @@ mod tests {
// Test reject rem // Test reject rem
test_acp_modify!(&me_rem, vec![acp_allow.clone()], &r2_set, false); test_acp_modify!(&me_rem, vec![acp_allow.clone()], &r2_set, false);
// Test reject purge // 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] #[test]

View file

@ -1,4 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use hashbrown::HashMap;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use super::profiles::AccessControlModify; use super::profiles::AccessControlModify;
@ -19,7 +20,7 @@ pub(super) enum ModifyResult<'a> {
pub(super) fn apply_modify_access<'a>( pub(super) fn apply_modify_access<'a>(
ident: &Identity, ident: &Identity,
related_acp: &'a [(&AccessControlModify, Filter<FilterValidResolved>)], related_acp: &'a [(&AccessControlModify, Filter<FilterValidResolved>)],
// may need sync agreements later. sync_agreements: &'a HashMap<Uuid, BTreeSet<String>>,
entry: &'a Arc<EntrySealedCommitted>, entry: &'a Arc<EntrySealedCommitted>,
) -> ModifyResult<'a> { ) -> ModifyResult<'a> {
let mut denied = false; let mut denied = false;
@ -46,7 +47,7 @@ pub(super) fn apply_modify_access<'a>(
// Check with protected if we should proceed. // Check with protected if we should proceed.
// If it's a sync entry, constrain it. // If it's a sync entry, constrain it.
match modify_sync_constrain(ident, entry) { match modify_sync_constrain(ident, entry, sync_agreements) {
AccessResult::Denied => denied = true, AccessResult::Denied => denied = true,
AccessResult::Constrain(mut set) => { AccessResult::Constrain(mut set) => {
constrain_rem.extend(set.iter().copied()); 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>( fn modify_sync_constrain<'a>(
ident: &Identity, ident: &Identity,
entry: &'a Arc<EntrySealedCommitted>, entry: &'a Arc<EntrySealedCommitted>,
sync_agreements: &'a HashMap<Uuid, BTreeSet<String>>,
) -> AccessResult<'a> { ) -> AccessResult<'a> {
match &ident.origin { match &ident.origin {
IdentType::Internal => AccessResult::Ignore, IdentType::Internal => AccessResult::Ignore,
@ -197,22 +199,34 @@ fn modify_sync_constrain<'a>(
AccessResult::Ignore AccessResult::Ignore
} }
IdentType::User(_) => { IdentType::User(_) => {
if let Some(classes) = entry.get_ava_set("class") { // We need to meet these conditions.
// If the entry is sync object. // * We are a sync object
if classes.contains(&PVCLASS_SYNC_OBJECT) { // * We have a sync_parent_uuid
// Constrain to a limited set of attributes. let is_sync = entry
AccessResult::Constrain(btreeset![ .get_ava_set("class")
"user_auth_token_session", .map(|classes| classes.contains(&PVCLASS_SYNC_OBJECT))
"oauth2_session", .unwrap_or(false);
"oauth2_consent_scope_map",
"credential_update_intent_token" if !is_sync {
]) return AccessResult::Ignore;
} else { }
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 { } else {
// Nothing to check. warn!(entry = ?entry.get_uuid(), "sync_parent_uuid not found on sync object, preventing all access");
AccessResult::Ignore AccessResult::Denied
} }
} }
} }

View file

@ -126,6 +126,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
.iter() .iter()
.any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)); .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 self.changed_uuid
.extend(commit_cand.iter().map(|e| e.get_uuid())); .extend(commit_cand.iter().map(|e| e.get_uuid()));
@ -134,6 +139,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
acp_reload = ?self.changed_acp, acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2, oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain, domain_reload = ?self.changed_domain,
changed_sync_agreement = ?self.changed_sync_agreement,
); );
// We are complete, finalise logging and return // We are complete, finalise logging and return

View file

@ -117,6 +117,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
.iter() .iter()
.any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)); .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 self.changed_uuid
.extend(del_cand.iter().map(|e| e.get_uuid())); .extend(del_cand.iter().map(|e| e.get_uuid()));
@ -126,6 +131,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
acp_reload = ?self.changed_acp, acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2, oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain, domain_reload = ?self.changed_domain,
changed_sync_agreement = ?self.changed_sync_agreement
); );
// Send result // Send result

View file

@ -435,7 +435,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
pub fn initialise_schema_idm(&mut self) -> Result<(), OperationError> { pub fn initialise_schema_idm(&mut self) -> Result<(), OperationError> {
admin_debug!("initialise_schema_idm -> start ..."); 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 let r: Result<(), _> = idm_schema_attrs
.into_iter() .into_iter()

View file

@ -6,7 +6,8 @@ use std::sync::Arc;
use concread::arcache::{ARCache, ARCacheBuilder, ARCacheReadTxn}; use concread::arcache::{ARCache, ARCacheBuilder, ARCacheReadTxn};
use concread::cowcell::*; use concread::cowcell::*;
use hashbrown::HashSet; use hashbrown::{HashMap, HashSet};
use std::collections::BTreeSet;
use tokio::sync::{Semaphore, SemaphorePermit}; use tokio::sync::{Semaphore, SemaphorePermit};
use tracing::trace; use tracing::trace;
@ -113,6 +114,7 @@ pub struct QueryServerWriteTransaction<'a> {
pub(crate) changed_acp: bool, pub(crate) changed_acp: bool,
pub(crate) changed_oauth2: bool, pub(crate) changed_oauth2: bool,
pub(crate) changed_domain: bool, pub(crate) changed_domain: bool,
changed_sync_agreement: bool,
// Store the list of changed uuids for other invalidation needs? // Store the list of changed uuids for other invalidation needs?
pub(crate) changed_uuid: HashSet<Uuid>, pub(crate) changed_uuid: HashSet<Uuid>,
_db_ticket: SemaphorePermit<'a>, _db_ticket: SemaphorePermit<'a>,
@ -1149,6 +1151,7 @@ impl QueryServer {
changed_acp: false, changed_acp: false,
changed_oauth2: false, changed_oauth2: false,
changed_domain: false, changed_domain: false,
changed_sync_agreement: false,
changed_uuid: HashSet::new(), changed_uuid: HashSet::new(),
_db_ticket: db_ticket, _db_ticket: db_ticket,
_write_ticket: write_ticket, _write_ticket: write_ticket,
@ -1172,7 +1175,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
&mut self.dyngroup_cache &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> { pub(crate) fn reload_schema(&mut self) -> Result<(), OperationError> {
// supply entries to the writable schema to reload from. // supply entries to the writable schema to reload from.
// find all attributes. // find all attributes.
@ -1245,6 +1248,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
Ok(()) Ok(())
} }
#[instrument(level = "debug", skip_all)]
fn reload_accesscontrols(&mut self) -> Result<(), OperationError> { fn reload_accesscontrols(&mut self) -> Result<(), OperationError> {
// supply entries to the writable access controls to reload from. // supply entries to the writable access controls to reload from.
// This has to be done in FOUR passes - one for each type! // This has to be done in FOUR passes - one for each type!
@ -1255,6 +1259,30 @@ impl<'a> QueryServerWriteTransaction<'a> {
// the entry lists themself. // the entry lists themself.
trace!("ACP reload started ..."); 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<Uuid, BTreeSet<String>> = 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 // Update search
let filt = filter!(f_and!([ let filt = filter!(f_and!([
f_eq("class", PVCLASS_ACP.clone()), f_eq("class", PVCLASS_ACP.clone()),
@ -1498,7 +1526,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
// based on any modifications that have occurred. // based on any modifications that have occurred.
// IF SCHEMA CHANGED WE MUST ALSO RELOAD!!! IE if schema had an attr removed // IF SCHEMA CHANGED WE MUST ALSO RELOAD!!! IE if schema had an attr removed
// that we rely on we MUST fail this here!! // 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()?; self.reload_accesscontrols()?;
} else { } else {
// On a reload the cache is dropped, otherwise we tell accesscontrols // On a reload the cache is dropped, otherwise we tell accesscontrols

View file

@ -210,6 +210,12 @@ impl<'a> QueryServerWriteTransaction<'a> {
.chain(pre_candidates.iter().map(|e| e.as_ref())) .chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)); .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( self.changed_uuid.extend(
norm_cand norm_cand
@ -223,6 +229,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
acp_reload = ?self.changed_acp, acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2, oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain, domain_reload = ?self.changed_domain,
changed_sync_agreement = ?self.changed_sync_agreement
); );
// return // return

View file

@ -13,6 +13,7 @@ impl SynchOpt {
| SynchOpt::ForceRefresh { copt, .. } | SynchOpt::ForceRefresh { copt, .. }
| SynchOpt::Finalise { copt, .. } | SynchOpt::Finalise { copt, .. }
| SynchOpt::Terminate { copt, .. } | SynchOpt::Terminate { copt, .. }
| SynchOpt::SetYieldAttributes { copt, .. }
| SynchOpt::SetCredentialPortal { copt, .. } => copt.debug, | SynchOpt::SetCredentialPortal { copt, .. } => copt.debug,
} }
} }
@ -83,6 +84,20 @@ impl SynchOpt {
Err(e) => error!("Error -> {:?}", e), 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 } => { SynchOpt::ForceRefresh { account_id, copt } => {
let client = copt.to_client(OpType::Write).await; let client = copt.to_client(OpType::Write).await;
match client.idm_sync_account_force_refresh(account_id).await { match client.idm_sync_account_force_refresh(account_id).await {

View file

@ -856,6 +856,18 @@ pub enum SynchOpt {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, 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<String>,
},
/// Reset the sync cookie of this connector, so that on the next operation of the sync tool /// 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 /// a full refresh of the provider is requested. Kanidm attributes that have been granted
/// authority will *not* be lost or deleted. /// authority will *not* be lost or deleted.