mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-17 14:33:55 +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()
|
Attribute::GidNumber.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum GroupAttrSchema {
|
||||||
|
Rfc2307,
|
||||||
|
#[default]
|
||||||
|
Rfc2307Bis,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub sync_token: String,
|
pub sync_token: String,
|
||||||
|
@ -102,6 +110,8 @@ pub struct Config {
|
||||||
pub group_attr_gidnumber: String,
|
pub group_attr_gidnumber: String,
|
||||||
#[serde(default = "group_attr_member")]
|
#[serde(default = "group_attr_member")]
|
||||||
pub group_attr_member: String,
|
pub group_attr_member: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub group_attr_schema: GroupAttrSchema,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub entry_map: BTreeMap<Uuid, EntryConfig>,
|
pub entry_map: BTreeMap<Uuid, EntryConfig>,
|
||||||
|
|
|
@ -11,16 +11,26 @@
|
||||||
// We allow expect since it forces good error messages at the least.
|
// We allow expect since it forces good error messages at the least.
|
||||||
#![allow(clippy::expect_used)]
|
#![allow(clippy::expect_used)]
|
||||||
|
|
||||||
mod config;
|
use crate::config::{Config, EntryConfig, GroupAttrSchema};
|
||||||
mod error;
|
|
||||||
|
|
||||||
use crate::config::{Config, EntryConfig};
|
|
||||||
use crate::error::SyncError;
|
use crate::error::SyncError;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cron::Schedule;
|
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::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 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::metadata;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
@ -38,22 +48,12 @@ use tokio::net::TcpListener;
|
||||||
use tokio::runtime;
|
use tokio::runtime;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::{fmt, EnvFilter};
|
use tracing_subscriber::{fmt, EnvFilter};
|
||||||
|
|
||||||
use kanidm_client::KanidmClientBuilder;
|
mod config;
|
||||||
use kanidm_lib_file_permissions::readonly as file_permissions_readonly;
|
mod error;
|
||||||
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};
|
|
||||||
|
|
||||||
include!("./opt.rs");
|
include!("./opt.rs");
|
||||||
|
|
||||||
|
@ -343,7 +343,7 @@ async fn run_sync(
|
||||||
LdapSyncRepl::Success {
|
LdapSyncRepl::Success {
|
||||||
cookie,
|
cookie,
|
||||||
refresh_deletes: _,
|
refresh_deletes: _,
|
||||||
entries,
|
mut entries,
|
||||||
delete_uuids,
|
delete_uuids,
|
||||||
present_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 {
|
let entries = match process_ldap_sync_result(entries, sync_config).await {
|
||||||
Ok(ssr) => ssr,
|
Ok(ssr) => ssr,
|
||||||
Err(()) => {
|
Err(()) => {
|
||||||
|
@ -444,6 +452,99 @@ async fn run_sync(
|
||||||
// done!
|
// 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(
|
async fn process_ldap_sync_result(
|
||||||
ldap_entries: Vec<LdapSyncReplEntry>,
|
ldap_entries: Vec<LdapSyncReplEntry>,
|
||||||
sync_config: &Config,
|
sync_config: &Config,
|
||||||
|
|
Loading…
Reference in a new issue