mirror of
https://github.com/kanidm/kanidm.git
synced 2025-04-23 02:35:38 +02:00
Support rfc2307 memberUid in sync operations. (#3466)
A lot of legacy directory servers will use rfc2307 schema where members of groups are stored as the uid instead of a dn. Within kani, we absolutely need this to be a dn, else we risk accidentally adding kanidm entries into ldap synced groups which isn't what we want. If we have an rfc2307 schema, then we pre-resolve the uid to the member dn so that kanidm gets the correct information.
This commit is contained in:
parent
4b4e690642
commit
e3243ce6b0
|
@ -59,6 +59,14 @@ fn group_attr_gidnumber() -> String {
|
|||
Attribute::GidNumber.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GroupAttrSchema {
|
||||
Rfc2307,
|
||||
#[default]
|
||||
Rfc2307Bis,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub sync_token: String,
|
||||
|
@ -102,6 +110,8 @@ pub struct Config {
|
|||
pub group_attr_gidnumber: String,
|
||||
#[serde(default = "group_attr_member")]
|
||||
pub group_attr_member: String,
|
||||
#[serde(default)]
|
||||
pub group_attr_schema: GroupAttrSchema,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub entry_map: BTreeMap<Uuid, EntryConfig>,
|
||||
|
|
|
@ -11,16 +11,26 @@
|
|||
// We allow expect since it forces good error messages at the least.
|
||||
#![allow(clippy::expect_used)]
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
|
||||
use crate::config::{Config, EntryConfig};
|
||||
use crate::config::{Config, EntryConfig, GroupAttrSchema};
|
||||
use crate::error::SyncError;
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use cron::Schedule;
|
||||
use kanidm_client::KanidmClientBuilder;
|
||||
use kanidm_lib_file_permissions::readonly as file_permissions_readonly;
|
||||
use kanidm_proto::constants::ATTR_OBJECTCLASS;
|
||||
use kanidm_proto::scim_v1::{
|
||||
MultiValueAttr, ScimEntry, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest,
|
||||
ScimSyncRetentionMode, ScimSyncState,
|
||||
};
|
||||
#[cfg(target_family = "unix")]
|
||||
use kanidm_utils_users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
|
||||
use kanidmd_lib::prelude::Attribute;
|
||||
use ldap3_client::{
|
||||
proto::{self, LdapFilter},
|
||||
LdapClient, LdapClientBuilder, LdapSyncRepl, LdapSyncReplEntry, LdapSyncStateValue,
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs::metadata;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
@ -38,22 +48,12 @@ use tokio::net::TcpListener;
|
|||
use tokio::runtime;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
use kanidm_client::KanidmClientBuilder;
|
||||
use kanidm_lib_file_permissions::readonly as file_permissions_readonly;
|
||||
use kanidm_proto::scim_v1::{
|
||||
MultiValueAttr, ScimEntry, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest,
|
||||
ScimSyncRetentionMode, ScimSyncState,
|
||||
};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
use kanidm_utils_users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
|
||||
|
||||
use ldap3_client::{proto, LdapClientBuilder, LdapSyncRepl, LdapSyncReplEntry, LdapSyncStateValue};
|
||||
mod config;
|
||||
mod error;
|
||||
|
||||
include!("./opt.rs");
|
||||
|
||||
|
@ -343,7 +343,7 @@ async fn run_sync(
|
|||
LdapSyncRepl::Success {
|
||||
cookie,
|
||||
refresh_deletes: _,
|
||||
entries,
|
||||
mut entries,
|
||||
delete_uuids,
|
||||
present_uuids,
|
||||
} => {
|
||||
|
@ -393,6 +393,14 @@ async fn run_sync(
|
|||
}
|
||||
};
|
||||
|
||||
if matches!(sync_config.group_attr_schema, GroupAttrSchema::Rfc2307) {
|
||||
// Since the schema is rfc 2307, this means that the names of members
|
||||
// in any group are uids, not dn's, so we need to resolve these now.
|
||||
resolve_member_uid_to_dn(&mut ldap_client, &mut entries, sync_config)
|
||||
.await
|
||||
.map_err(|_| SyncError::Preprocess)?;
|
||||
};
|
||||
|
||||
let entries = match process_ldap_sync_result(entries, sync_config).await {
|
||||
Ok(ssr) => ssr,
|
||||
Err(()) => {
|
||||
|
@ -444,6 +452,99 @@ async fn run_sync(
|
|||
// done!
|
||||
}
|
||||
|
||||
async fn resolve_member_uid_to_dn(
|
||||
ldap_client: &mut LdapClient,
|
||||
ldap_entries: &mut [LdapSyncReplEntry],
|
||||
sync_config: &Config,
|
||||
) -> Result<(), ()> {
|
||||
let mut lookup_cache: BTreeMap<String, String> = Default::default();
|
||||
|
||||
for sync_entry in ldap_entries.iter_mut() {
|
||||
let oc = sync_entry
|
||||
.entry
|
||||
.attrs
|
||||
.get(ATTR_OBJECTCLASS)
|
||||
.ok_or_else(|| {
|
||||
error!("Invalid entry - no object class {}", sync_entry.entry.dn);
|
||||
})?;
|
||||
|
||||
if !oc.contains(&sync_config.group_objectclass) {
|
||||
// Not a group, skip.
|
||||
continue;
|
||||
}
|
||||
|
||||
// It's a group, does it have memberUid? We pop this out here
|
||||
// because we plan to replace it.
|
||||
let members = sync_entry
|
||||
.entry
|
||||
.remove_ava(&sync_config.group_attr_member)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Now, search all the members to dns.
|
||||
let mut resolved_members: BTreeSet<String> = Default::default();
|
||||
|
||||
for member_uid in members {
|
||||
if let Some(member_dn) = lookup_cache.get(&member_uid) {
|
||||
resolved_members.insert(member_dn.to_string());
|
||||
} else {
|
||||
// Not in cache, search it. We use a syncrepl request here as this
|
||||
// can bypass some query limits. Note we set the sync cookie to None.
|
||||
let filter = LdapFilter::And(vec![
|
||||
// Always put uid first as openldap can't query optimise.
|
||||
LdapFilter::Equality(
|
||||
sync_config.person_attr_user_name.clone(),
|
||||
member_uid.clone(),
|
||||
),
|
||||
LdapFilter::Equality(
|
||||
ATTR_OBJECTCLASS.into(),
|
||||
sync_config.person_objectclass.clone(),
|
||||
),
|
||||
]);
|
||||
|
||||
let mode = proto::SyncRequestMode::RefreshOnly;
|
||||
let sync_result = ldap_client
|
||||
.syncrepl(sync_config.ldap_sync_base_dn.clone(), filter, None, mode)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
debug!(?member_uid, ?sync_entry.entry_uuid);
|
||||
error!(
|
||||
?err,
|
||||
"Failed to perform syncrepl to resolve members from ldap"
|
||||
);
|
||||
})?;
|
||||
|
||||
// Get the memberDN out now.
|
||||
let member_dn = match sync_result {
|
||||
LdapSyncRepl::Success { mut entries, .. } => {
|
||||
let Some(resolved_entry) = entries.pop() else {
|
||||
warn!(?member_uid, "Unable to resolve member, no matching entries");
|
||||
continue;
|
||||
};
|
||||
|
||||
resolved_entry.entry.dn.clone()
|
||||
}
|
||||
_ => {
|
||||
error!("Invalid sync repl result state");
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
// cache it.
|
||||
lookup_cache.insert(member_uid, member_dn.clone());
|
||||
resolved_members.insert(member_dn);
|
||||
}
|
||||
}
|
||||
|
||||
// Put the members back in resolved as DN's now.
|
||||
sync_entry
|
||||
.entry
|
||||
.attrs
|
||||
.insert(sync_config.group_attr_member.clone(), resolved_members);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_ldap_sync_result(
|
||||
ldap_entries: Vec<LdapSyncReplEntry>,
|
||||
sync_config: &Config,
|
||||
|
|
Loading…
Reference in a new issue