mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +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:
|
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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")),
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue