mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
1785 allow sync attr yielding via partial write admin (#1879)
This commit is contained in:
parent
578e064b10
commit
79ff5e9775
|
@ -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]
|
||||
|
|
|
@ -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 <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
|
||||
|
||||
If you are performing a migration from an external IDM to Kanidm, when that migration is completed
|
||||
|
|
|
@ -42,6 +42,19 @@ impl KanidmClient {
|
|||
.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(
|
||||
&self,
|
||||
name: &str,
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<Uuid, &ScimEntry> = 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);
|
||||
|
|
|
@ -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<AccessControlCreate>,
|
||||
acps_modify: Vec<AccessControlModify>,
|
||||
acps_delete: Vec<AccessControlDelete>,
|
||||
sync_agreements: HashMap<Uuid, BTreeSet<String>>,
|
||||
// Oauth2
|
||||
// Sync prov
|
||||
}
|
||||
|
@ -106,6 +108,7 @@ pub trait AccessControlsTransaction<'a> {
|
|||
fn get_create(&self) -> &Vec<AccessControlCreate>;
|
||||
fn get_modify(&self) -> &Vec<AccessControlModify>;
|
||||
fn get_delete(&self) -> &Vec<AccessControlDelete>;
|
||||
fn get_sync_agreements(&self) -> &HashMap<Uuid, BTreeSet<String>>;
|
||||
|
||||
#[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<Uuid, BTreeSet<String>>) {
|
||||
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<Uuid, BTreeSet<String>> {
|
||||
&self.inner.sync_agreements
|
||||
}
|
||||
|
||||
fn get_acp_resolve_filter_cache(
|
||||
&self,
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
|
||||
|
@ -969,6 +992,10 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsReadTransaction<'a> {
|
|||
&self.inner.acps_delete
|
||||
}
|
||||
|
||||
fn get_sync_agreements(&self) -> &HashMap<Uuid, BTreeSet<String>> {
|
||||
&self.inner.sync_agreements
|
||||
}
|
||||
|
||||
fn get_acp_resolve_filter_cache(
|
||||
&self,
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
|
||||
|
@ -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]
|
||||
|
|
|
@ -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<FilterValidResolved>)],
|
||||
// may need sync agreements later.
|
||||
sync_agreements: &'a HashMap<Uuid, BTreeSet<String>>,
|
||||
entry: &'a Arc<EntrySealedCommitted>,
|
||||
) -> 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<EntrySealedCommitted>,
|
||||
sync_agreements: &'a HashMap<Uuid, BTreeSet<String>>,
|
||||
) -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<Uuid>,
|
||||
_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<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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<String>,
|
||||
},
|
||||
/// 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.
|
||||
|
|
Loading…
Reference in a new issue