From 4dfee43e664414acc1822797ea030adbb05f7648 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Wed, 15 May 2024 13:32:07 -0600 Subject: [PATCH 01/10] Specify in conf that pam_allow_groups is required Signed-off-by: David Mulder --- src/config/himmelblau.conf.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/himmelblau.conf.example b/src/config/himmelblau.conf.example index c1d15e9..bb4a3fa 100644 --- a/src/config/himmelblau.conf.example +++ b/src/config/himmelblau.conf.example @@ -4,14 +4,14 @@ # domain will be the owner of the device object in the directory. Typically # this would be the primary user of the device. # domains = - -### Optional global values +# # pam_allow_groups MUST be defined or all users will be rejected by pam account. # The option should be set to a comma seperated list of Users and Groups which # are allowed access to the system. The first user to logon (the device owner) # to a configured domain will be added to this list automatically. # pam_allow_groups = # +### Optional global values # If you have an ODC provider (the default being odc.officeapps.live.com), specify # the hostname for sending a federationProvider request. If the federationProvider # request is successful, the tenant_id and authority_host options do not need to From efd1805ea75357c9c7783320930a5ecb3962ea7a Mon Sep 17 00:00:00 2001 From: David Mulder Date: Wed, 21 Feb 2024 08:26:58 -0700 Subject: [PATCH 02/10] Use the SSSD Idmap code in Himmelblau This builds Sumit's experimental idmapping code for EntraID. Signed-off-by: David Mulder --- .gitmodules | 4 + Cargo.toml | 2 + src/common/Cargo.toml | 1 + src/common/src/config.rs | 23 ++- src/common/src/constants.rs | 3 +- src/common/src/idmap.rs | 74 -------- src/common/src/idprovider/himmelblau.rs | 65 +++++-- src/common/src/lib.rs | 2 - src/config/himmelblau.conf.example | 5 + src/idmap/.gitignore | 10 + src/idmap/Cargo.toml | 23 +++ src/idmap/autogen.sh | 3 + src/idmap/build.rs | 53 ++++++ src/idmap/configure.ac | 17 ++ src/idmap/src/lib.rs | 241 ++++++++++++++++++++++++ src/idmap/sssd | 1 + src/idmap/wrapper.h | 1 + 17 files changed, 435 insertions(+), 93 deletions(-) delete mode 100644 src/common/src/idmap.rs create mode 100644 src/idmap/.gitignore create mode 100644 src/idmap/Cargo.toml create mode 100755 src/idmap/autogen.sh create mode 100644 src/idmap/build.rs create mode 100644 src/idmap/configure.ac create mode 100644 src/idmap/src/lib.rs create mode 160000 src/idmap/sssd create mode 100644 src/idmap/wrapper.h diff --git a/.gitmodules b/.gitmodules index 34b776d..d58dbbf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,3 +3,7 @@ url = https://github.com/kanidm/kanidm.git branch = master shallow = true +[submodule "src/idmap/sssd"] + path = src/idmap/sssd + url = https://github.com/dmulder/sssd.git + branch = sss_idmap_4_idp diff --git a/Cargo.toml b/Cargo.toml index 3b49228..f25205b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "src/proto", "src/kanidm/libs/crypto", "src/kanidm/libs/users", + "src/idmap", ] resolver = "2" @@ -52,6 +53,7 @@ chrono = "^0.4.31" os-release = "^0.1.0" jsonwebtoken = "^9.2.0" zeroize = "^1.7.0" +idmap = { path = "src/idmap" } # Kanidm deps argon2 = { version = "0.5.2", features = ["alloc"] } diff --git a/src/common/Cargo.toml b/src/common/Cargo.toml index db476a3..6828402 100644 --- a/src/common/Cargo.toml +++ b/src/common/Cargo.toml @@ -28,6 +28,7 @@ kanidm-hsm-crypto = { workspace = true } compact_jwt = { workspace = true } os-release = { workspace = true } zeroize = { workspace = true } +idmap = { workspace = true } # Kanidm deps rusqlite = { workspace = true } diff --git a/src/common/src/config.rs b/src/common/src/config.rs index fcba84a..9a49b0b 100644 --- a/src/common/src/config.rs +++ b/src/common/src/config.rs @@ -8,15 +8,22 @@ use tracing::{debug, error}; use crate::constants::{ DEFAULT_AUTHORITY_HOST, DEFAULT_CACHE_TIMEOUT, DEFAULT_CONFIG_PATH, DEFAULT_CONN_TIMEOUT, DEFAULT_DB_PATH, DEFAULT_GRAPH, DEFAULT_HELLO_ENABLED, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, - DEFAULT_HOME_PREFIX, DEFAULT_HSM_PIN_PATH, DEFAULT_IDMAP_RANGE, DEFAULT_ODC_PROVIDER, + DEFAULT_HOME_PREFIX, DEFAULT_HSM_PIN_PATH, DEFAULT_ID_ATTR_MAP, DEFAULT_ODC_PROVIDER, DEFAULT_SELINUX, DEFAULT_SHELL, DEFAULT_SOCK_PATH, DEFAULT_TASK_SOCK_PATH, DEFAULT_USE_ETC_SKEL, SERVER_CONFIG_PATH, }; use crate::unix_config::{HomeAttr, HsmType}; use graph::constants::BROKER_APP_ID; use graph::misc::request_federation_provider; +use idmap::DEFAULT_IDMAP_RANGE; use std::env; +#[derive(Debug, Copy, Clone)] +pub enum IdAttr { + Uuid, + Name, +} + pub fn split_username(username: &str) -> Option<(&str, &str)> { let tup: Vec<&str> = username.split('@').collect(); if tup.len() == 2 { @@ -456,6 +463,20 @@ impl HimmelblauConfig { DEFAULT_HELLO_ENABLED, ) } + + pub fn get_id_attr_map(&self) -> IdAttr { + match self.config.get("global", "id_attr_map") { + Some(id_attr_map) => match id_attr_map.to_lowercase().as_str() { + "uuid" => IdAttr::Uuid, + "name" => IdAttr::Name, + _ => { + error!("Unrecognized id_attr_map choice: {}", id_attr_map); + DEFAULT_ID_ATTR_MAP + } + }, + None => DEFAULT_ID_ATTR_MAP, + } + } } impl fmt::Debug for HimmelblauConfig { diff --git a/src/common/src/constants.rs b/src/common/src/constants.rs index 489311a..856efbe 100644 --- a/src/common/src/constants.rs +++ b/src/common/src/constants.rs @@ -1,3 +1,4 @@ +use crate::config::IdAttr; use crate::unix_config::HomeAttr; pub const DEFAULT_CONFIG_PATH: &str = "/etc/himmelblau/himmelblau.conf"; @@ -15,9 +16,9 @@ pub const DEFAULT_AUTHORITY_HOST: &str = "login.microsoftonline.com"; pub const DEFAULT_GRAPH: &str = "https://graph.microsoft.com"; pub const DEFAULT_APP_ID: &str = "b743a22d-6705-4147-8670-d92fa515ee2b"; pub const DRS_APP_ID: &str = "01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9"; -pub const DEFAULT_IDMAP_RANGE: (u32, u32) = (1000000, 6999999); pub const DEFAULT_CONN_TIMEOUT: u64 = 30; pub const DEFAULT_CACHE_TIMEOUT: u64 = 15; pub const DEFAULT_SELINUX: bool = true; pub const DEFAULT_HSM_PIN_PATH: &str = "/var/lib/himmelblaud/hsm-pin"; pub const DEFAULT_HELLO_ENABLED: bool = true; +pub const DEFAULT_ID_ATTR_MAP: IdAttr = IdAttr::Name; diff --git a/src/common/src/idmap.rs b/src/common/src/idmap.rs deleted file mode 100644 index 1217b47..0000000 --- a/src/common/src/idmap.rs +++ /dev/null @@ -1,74 +0,0 @@ -use uuid::Uuid; - -#[allow(non_camel_case_types)] -#[derive(Debug)] -pub enum IdmapError { - IDMAP_NOT_IMPLEMENTED, - IDMAP_ERROR, - IDMAP_SID_INVALID, - IDMAP_NO_RANGE, - IDMAP_COLLISION, -} - -#[allow(dead_code)] -struct AadSid { - sid_rev_num: u8, - num_auths: i8, - id_auth: u64, // Technically only 48 bits - sub_auths: [u32; 15], -} - -fn object_id_to_sid(object_id: &Uuid) -> Result { - let bytes_array = object_id.as_bytes(); - let s_bytes_array = [ - bytes_array[6], - bytes_array[7], - bytes_array[4], - bytes_array[5], - ]; - - let mut sid = AadSid { - sid_rev_num: 1, - num_auths: 5, - id_auth: 12, - sub_auths: [0; 15], - }; - - sid.sub_auths[0] = 1; - sid.sub_auths[1] = u32::from_be_bytes( - bytes_array[0..4] - .try_into() - .map_err(|_| IdmapError::IDMAP_SID_INVALID)?, - ); - sid.sub_auths[2] = u32::from_be_bytes(s_bytes_array); - sid.sub_auths[3] = u32::from_le_bytes( - bytes_array[8..12] - .try_into() - .map_err(|_| IdmapError::IDMAP_SID_INVALID)?, - ); - sid.sub_auths[4] = u32::from_le_bytes( - bytes_array[12..] - .try_into() - .map_err(|_| IdmapError::IDMAP_SID_INVALID)?, - ); - - Ok(sid) -} - -fn rid_from_sid(sid: &AadSid) -> Result { - Ok(sid.sub_auths - [usize::try_from(sid.num_auths).map_err(|_| IdmapError::IDMAP_SID_INVALID)? - 1]) -} - -pub(crate) fn object_id_to_unix_id( - object_id: &Uuid, - idmap_range: (u32, u32), -) -> Result { - let sid = object_id_to_sid(object_id)?; - let rid = rid_from_sid(&sid)?; - if idmap_range.0 >= idmap_range.1 { - return Err(IdmapError::IDMAP_NO_RANGE); - } - let uid_count = idmap_range.1 - idmap_range.0; - Ok((rid % uid_count) + idmap_range.0) -} diff --git a/src/common/src/idprovider/himmelblau.rs b/src/common/src/idprovider/himmelblau.rs index b301ed2..fd3277b 100644 --- a/src/common/src/idprovider/himmelblau.rs +++ b/src/common/src/idprovider/himmelblau.rs @@ -4,14 +4,15 @@ use super::interface::{ }; use crate::config::split_username; use crate::config::HimmelblauConfig; +use crate::config::IdAttr; use crate::db::KeyStoreTxn; -use crate::idmap::object_id_to_unix_id; use crate::idprovider::interface::tpm; use crate::unix_proto::{DeviceAuthorizationResponse, PamAuthRequest}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use graph::user::{request_user_groups, DirectoryObject}; use himmelblau_policies::policies::apply_group_policy; +use idmap::SssIdmap; use kanidm_hsm_crypto::{LoadableIdentityKey, LoadableMsOapxbcRsaKey, PinValue, SealedData, Tpm}; use msal::auth::{ BrokerClientApplication, ClientInfo, @@ -82,21 +83,38 @@ impl HimmelblauMultiProvider { Ok(config) => Arc::new(RwLock::new(config)), Err(e) => return Err(anyhow!("{}", e)), }; + let idmap = match SssIdmap::new() { + Ok(idmap) => Arc::new(RwLock::new(idmap)), + Err(e) => return Err(anyhow!("{:?}", e)), + }; let mut providers = HashMap::new(); let cfg = config.read().await; for domain in cfg.get_configured_domains() { debug!("Adding provider for domain {}", domain); + let range = cfg.get_idmap_range(&domain); + let mut idmap_lk = idmap.write().await; let (authority_host, tenant_id, graph) = match cfg.get_tenant_id_authority_and_graph(&domain).await { Ok(res) => res, Err(e) => return Err(anyhow!("{}", e)), }; + idmap_lk + .add_gen_domain(&domain, &tenant_id, range) + .map_err(|e| anyhow!("{:?}", e))?; let authority_url = format!("https://{}/{}", authority_host, tenant_id); let app = BrokerClientApplication::new(Some(authority_url.as_str()), None, None) .map_err(|e| anyhow!("{:?}", e))?; - let provider = - HimmelblauProvider::new(app, &config, &tenant_id, &domain, &authority_host, &graph); + let provider = HimmelblauProvider::new( + app, + &config, + &tenant_id, + &domain, + &authority_host, + &graph, + &idmap, + ) + .map_err(|_| anyhow!("Failed to initialize the provider"))?; { let mut client = provider.client.write().await; if let Ok(transport_key) = @@ -315,6 +333,7 @@ pub struct HimmelblauProvider { authority_host: String, graph_url: String, refresh_cache: RefreshCache, + idmap: Arc>, } impl HimmelblauProvider { @@ -325,8 +344,9 @@ impl HimmelblauProvider { domain: &str, authority_host: &str, graph_url: &str, - ) -> Self { - HimmelblauProvider { + idmap: &Arc>, + ) -> Result { + Ok(HimmelblauProvider { client: RwLock::new(client), config: config.clone(), tenant_id: tenant_id.to_string(), @@ -334,7 +354,8 @@ impl HimmelblauProvider { authority_host: authority_host.to_string(), graph_url: graph_url.to_string(), refresh_cache: RefreshCache::new(), - } + idmap: idmap.clone(), + }) } } @@ -1345,11 +1366,19 @@ impl HimmelblauProvider { }; let sshkeys: Vec = vec![]; let valid = true; - let idmap_range = config.get_idmap_range(&self.domain); - let gidnumber = object_id_to_unix_id(&uuid, idmap_range).map_err(|e| { - debug!("Failed mapping uuid to unix uid/gid: {:?}", e); - IdpError::BadRequest - })?; + let idmap = self.idmap.read().await; + let gidnumber = match config.get_id_attr_map() { + IdAttr::Uuid => idmap + .object_id_to_unix_id(&self.tenant_id, &uuid) + .map_err(|e| { + error!("{:?}", e); + IdpError::BadRequest + })?, + IdAttr::Name => idmap.gen_to_unix(&self.tenant_id, &spn).map_err(|e| { + error!("{:?}", e); + IdpError::BadRequest + })?, + }; // Add the fake primary group groups.push(GroupToken { @@ -1376,6 +1405,7 @@ impl HimmelblauProvider { &self, value: DirectoryObject, ) -> Result { + let config = self.config.read().await; let name = match value.get("display_name") { Some(name) => name, None => return Err(anyhow!("Failed retrieving group display_name")), @@ -1386,10 +1416,15 @@ impl HimmelblauProvider { } None => return Err(anyhow!("Failed retrieving group uuid")), }; - let config = self.config.read(); - let idmap_range = config.await.get_idmap_range(&self.domain); - let gidnumber = object_id_to_unix_id(&id, idmap_range) - .map_err(|e| anyhow!("Failed mapping uuid to unix gid: {:?}", e))?; + let idmap = self.idmap.read().await; + let gidnumber = match config.get_id_attr_map() { + IdAttr::Uuid => idmap + .object_id_to_unix_id(&self.tenant_id, &id) + .map_err(|e| anyhow!("Failed fetching gid for {}: {:?}", id, e))?, + IdAttr::Name => idmap + .gen_to_unix(&self.tenant_id, name) + .map_err(|e| anyhow!("Failed fetching gid for {}: {:?}", name, e))?, + }; Ok(GroupToken { name: name.clone(), diff --git a/src/common/src/lib.rs b/src/common/src/lib.rs index cbd506f..c983440 100644 --- a/src/common/src/lib.rs +++ b/src/common/src/lib.rs @@ -22,8 +22,6 @@ pub mod config; #[cfg(target_family = "unix")] pub mod constants; #[cfg(target_family = "unix")] -pub mod idmap; -#[cfg(target_family = "unix")] pub mod unix_config; // Kanidm modules diff --git a/src/config/himmelblau.conf.example b/src/config/himmelblau.conf.example index bb4a3fa..7ccf31d 100644 --- a/src/config/himmelblau.conf.example +++ b/src/config/himmelblau.conf.example @@ -12,6 +12,11 @@ # pam_allow_groups = # ### Optional global values +# Specify whether to map uid/gid based on the object name or the object uuid. +# By object uuid mapping is the old default, but can cause authentication +# issues over SSH. Mapping by name is recommeneded. +# id_attr_map = name ; {name|uuid} +# # If you have an ODC provider (the default being odc.officeapps.live.com), specify # the hostname for sending a federationProvider request. If the federationProvider # request is successful, the tenant_id and authority_host options do not need to diff --git a/src/idmap/.gitignore b/src/idmap/.gitignore new file mode 100644 index 0000000..de93b03 --- /dev/null +++ b/src/idmap/.gitignore @@ -0,0 +1,10 @@ +config.h +aclocal.m4 +autom4te.cache/ +config.h.in +config.h.in~ +config.log +config.status +configure +configure.ac~ +configure~ diff --git a/src/idmap/Cargo.toml b/src/idmap/Cargo.toml new file mode 100644 index 0000000..dce7112 --- /dev/null +++ b/src/idmap/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "idmap" + +version.workspace = true +authors.workspace = true +rust-version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lib] +name = "idmap" +path = "src/lib.rs" + +[dependencies] +libc = "0.2.153" +tracing = { workspace = true } +uuid = { workspace = true } + +[build-dependencies] +cc = "1.0.97" +bindgen = "0.69.4" diff --git a/src/idmap/autogen.sh b/src/idmap/autogen.sh new file mode 100755 index 0000000..905970e --- /dev/null +++ b/src/idmap/autogen.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +autoreconf -if diff --git a/src/idmap/build.rs b/src/idmap/build.rs new file mode 100644 index 0000000..b941c0c --- /dev/null +++ b/src/idmap/build.rs @@ -0,0 +1,53 @@ +use std::env; +use std::io::{self, Write}; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let autoreconf = Command::new("./autogen.sh") + .output() + .expect("Failed to configure sss_idmap"); + if !autoreconf.status.success() { + io::stdout().write_all(&autoreconf.stdout).unwrap(); + io::stderr().write_all(&autoreconf.stderr).unwrap(); + panic!("Failed to configure sss_idmap"); + } + io::stdout().write_all(&autoreconf.stdout).unwrap(); + let configure = Command::new("./configure") + .output() + .expect("Failed to configure sss_idmap"); + if !configure.status.success() { + io::stdout().write_all(&configure.stdout).unwrap(); + io::stderr().write_all(&configure.stderr).unwrap(); + panic!("Failed to configure sss_idmap"); + } + io::stdout().write_all(&configure.stdout).unwrap(); + + cc::Build::new() + .file("sssd/src/lib/idmap/sss_idmap.c") + .file("sssd/src/lib/idmap/sss_idmap_conv.c") + .file("sssd/src/util/murmurhash3.c") + .include(Path::new("sssd/src")) + .include(Path::new("./")) // for config.h + .warnings(false) + .compile("sss_idmap"); + + let bindings = bindgen::Builder::default() + .blocklist_function("qgcvt") + .blocklist_function("qgcvt_r") + .blocklist_function("qfcvt") + .blocklist_function("qfcvt_r") + .blocklist_function("qecvt") + .blocklist_function("qecvt_r") + .blocklist_function("strtold") + .header("wrapper.h") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .expect("Unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/src/idmap/configure.ac b/src/idmap/configure.ac new file mode 100644 index 0000000..8c54c9e --- /dev/null +++ b/src/idmap/configure.ac @@ -0,0 +1,17 @@ +AC_PREREQ([2.69]) + +m4_include([sssd/version.m4]) +AC_INIT([libsss_idmap],[VERSION_NUMBER],[sssd-devel@lists.fedorahosted.org]) + +AC_CONFIG_HEADERS([config.h]) + +AC_DEFINE_UNQUOTED( + [SSS_ATTRIBUTE_FALLTHROUGH], + [$sss_cv_attribute_fallthrough_val], + [__attribute__((fallthrough)) if supported]) + +m4_include([sssd/src/external/sizes.m4]) +m4_include([sssd/src/build_macros.m4]) +m4_include([sssd/src/external/libpcre.m4]) + +AC_OUTPUT diff --git a/src/idmap/src/lib.rs b/src/idmap/src/lib.rs new file mode 100644 index 0000000..2fa4015 --- /dev/null +++ b/src/idmap/src/lib.rs @@ -0,0 +1,241 @@ +#![deny(warnings)] +#![warn(unused_extern_crates)] +#![deny(clippy::todo)] +#![deny(clippy::unimplemented)] +#![deny(clippy::unwrap_used)] +#![deny(clippy::expect_used)] +#![deny(clippy::panic)] +#![deny(clippy::unreachable)] +#![deny(clippy::await_holding_lock)] +#![deny(clippy::needless_pass_by_value)] +#![deny(clippy::trivially_copy_pass_by_ref)] +use std::collections::HashMap; +use std::ffi::CString; +use std::ptr; +use std::sync::RwLock; +use uuid::Uuid; + +#[macro_use] +extern crate tracing; + +mod ffi { + #![allow(non_upper_case_globals)] + #![allow(non_camel_case_types)] + #![allow(non_snake_case)] + #![allow(dead_code)] + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +#[derive(Debug)] +#[allow(non_camel_case_types)] +pub enum IdmapError { + IDMAP_SUCCESS, + IDMAP_NOT_IMPLEMENTED, + IDMAP_ERROR, + IDMAP_OUT_OF_MEMORY, + IDMAP_NO_DOMAIN, + IDMAP_CONTEXT_INVALID, + IDMAP_SID_INVALID, + IDMAP_SID_UNKNOWN, + IDMAP_NO_RANGE, + IDMAP_BUILTIN_SID, + IDMAP_OUT_OF_SLICES, + IDMAP_COLLISION, + IDMAP_EXTERNAL, + IDMAP_NAME_UNKNOWN, + IDMAP_NO_REVERSE, + IDMAP_ERR_LAST, +} + +fn map_err(e: ffi::idmap_error_code) -> IdmapError { + match e { + ffi::idmap_error_code_IDMAP_SUCCESS => IdmapError::IDMAP_SUCCESS, + ffi::idmap_error_code_IDMAP_NOT_IMPLEMENTED => IdmapError::IDMAP_NOT_IMPLEMENTED, + ffi::idmap_error_code_IDMAP_ERROR => IdmapError::IDMAP_ERROR, + ffi::idmap_error_code_IDMAP_OUT_OF_MEMORY => IdmapError::IDMAP_OUT_OF_MEMORY, + ffi::idmap_error_code_IDMAP_NO_DOMAIN => IdmapError::IDMAP_NO_DOMAIN, + ffi::idmap_error_code_IDMAP_CONTEXT_INVALID => IdmapError::IDMAP_CONTEXT_INVALID, + ffi::idmap_error_code_IDMAP_SID_INVALID => IdmapError::IDMAP_SID_INVALID, + ffi::idmap_error_code_IDMAP_SID_UNKNOWN => IdmapError::IDMAP_SID_UNKNOWN, + ffi::idmap_error_code_IDMAP_NO_RANGE => IdmapError::IDMAP_NO_RANGE, + ffi::idmap_error_code_IDMAP_BUILTIN_SID => IdmapError::IDMAP_BUILTIN_SID, + ffi::idmap_error_code_IDMAP_OUT_OF_SLICES => IdmapError::IDMAP_OUT_OF_SLICES, + ffi::idmap_error_code_IDMAP_COLLISION => IdmapError::IDMAP_COLLISION, + ffi::idmap_error_code_IDMAP_EXTERNAL => IdmapError::IDMAP_EXTERNAL, + ffi::idmap_error_code_IDMAP_NAME_UNKNOWN => IdmapError::IDMAP_NAME_UNKNOWN, + ffi::idmap_error_code_IDMAP_NO_REVERSE => IdmapError::IDMAP_NO_REVERSE, + ffi::idmap_error_code_IDMAP_ERR_LAST => IdmapError::IDMAP_ERR_LAST, + _ => { + error!("Unknown error code '{}'", e); + IdmapError::IDMAP_ERROR + } + } +} + +#[allow(dead_code)] +struct AadSid { + sid_rev_num: u8, + num_auths: i8, + id_auth: u64, // Technically only 48 bits + sub_auths: [u32; 15], +} + +fn object_id_to_sid(object_id: &Uuid) -> Result { + let bytes_array = object_id.as_bytes(); + let s_bytes_array = [ + bytes_array[6], + bytes_array[7], + bytes_array[4], + bytes_array[5], + ]; + + let mut sid = AadSid { + sid_rev_num: 1, + num_auths: 5, + id_auth: 12, + sub_auths: [0; 15], + }; + + sid.sub_auths[0] = 1; + sid.sub_auths[1] = u32::from_be_bytes( + bytes_array[0..4] + .try_into() + .map_err(|_| IdmapError::IDMAP_SID_INVALID)?, + ); + sid.sub_auths[2] = u32::from_be_bytes(s_bytes_array); + sid.sub_auths[3] = u32::from_le_bytes( + bytes_array[8..12] + .try_into() + .map_err(|_| IdmapError::IDMAP_SID_INVALID)?, + ); + sid.sub_auths[4] = u32::from_le_bytes( + bytes_array[12..] + .try_into() + .map_err(|_| IdmapError::IDMAP_SID_INVALID)?, + ); + + Ok(sid) +} + +fn rid_from_sid(sid: &AadSid) -> Result { + Ok(sid.sub_auths + [usize::try_from(sid.num_auths).map_err(|_| IdmapError::IDMAP_SID_INVALID)? - 1]) +} + +pub const DEFAULT_IDMAP_RANGE: (u32, u32) = (200000, 2000200000); + +// The ctx is behind a read/write lock to make it 'safer' to Send/Sync. +// Granted, dereferencing a raw pointer is still inherently unsafe. +pub struct SssIdmap { + ctx: RwLock<*mut ffi::sss_idmap_ctx>, + ranges: HashMap, +} + +unsafe impl Send for SssIdmap {} +unsafe impl Sync for SssIdmap {} + +impl SssIdmap { + pub fn new() -> Result { + let mut ctx = ptr::null_mut(); + unsafe { + match map_err(ffi::sss_idmap_init(None, ptr::null_mut(), None, &mut ctx)) { + IdmapError::IDMAP_SUCCESS => Ok(SssIdmap { + ctx: RwLock::new(ctx), + ranges: HashMap::new(), + }), + e => Err(e), + } + } + } + + pub fn add_gen_domain( + &mut self, + domain_name: &str, + tenant_id: &str, + range: (u32, u32), + ) -> Result<(), IdmapError> { + let ctx = self.ctx.write().map_err(|e| { + error!("Failed obtaining write lock on sss_idmap_ctx: {}", e); + IdmapError::IDMAP_ERROR + })?; + let domain_name_cstr = + CString::new(domain_name).map_err(|_| IdmapError::IDMAP_OUT_OF_MEMORY)?; + let tenant_id_cstr = + CString::new(tenant_id).map_err(|_| IdmapError::IDMAP_OUT_OF_MEMORY)?; + let mut idmap_range = ffi::sss_idmap_range { + min: range.0, + max: range.1, + }; + self.ranges.insert(tenant_id.to_string(), range); + unsafe { + match map_err(ffi::sss_idmap_add_gen_domain_ex( + *ctx, + domain_name_cstr.as_ptr(), + tenant_id_cstr.as_ptr(), + &mut idmap_range, + ptr::null_mut(), + None, + None, + ptr::null_mut(), + 0, + false, + )) { + IdmapError::IDMAP_SUCCESS => Ok(()), + e => Err(e), + } + } + } + + pub fn gen_to_unix(&self, tenant_id: &str, input: &str) -> Result { + let ctx = self.ctx.write().map_err(|e| { + error!("Failed obtaining write lock on sss_idmap_ctx: {}", e); + IdmapError::IDMAP_ERROR + })?; + let tenant_id_cstr = + CString::new(tenant_id).map_err(|_| IdmapError::IDMAP_OUT_OF_MEMORY)?; + let input_cstr = CString::new(input).map_err(|_| IdmapError::IDMAP_OUT_OF_MEMORY)?; + unsafe { + let mut id: u32 = 0; + match map_err(ffi::sss_idmap_gen_to_unix( + *ctx, + tenant_id_cstr.as_ptr(), + input_cstr.as_ptr(), + &mut id, + )) { + IdmapError::IDMAP_SUCCESS => Ok(id), + e => Err(e), + } + } + } + + pub fn object_id_to_unix_id( + &self, + tenant_id: &str, + object_id: &Uuid, + ) -> Result { + let sid = object_id_to_sid(object_id)?; + let rid = rid_from_sid(&sid)?; + let idmap_range = match self.ranges.get(tenant_id) { + Some(idmap_range) => idmap_range, + None => return Err(IdmapError::IDMAP_NO_RANGE), + }; + let uid_count = idmap_range.1 - idmap_range.0; + Ok((rid % uid_count) + idmap_range.0) + } +} + +impl Drop for SssIdmap { + fn drop(&mut self) { + match self.ctx.write() { + Ok(ctx) => unsafe { + let _ = ffi::sss_idmap_free(*ctx); + }, + Err(e) => { + error!( + "Failed obtaining write lock on sss_idmap_ctx during drop: {}", + e + ); + } + } + } +} diff --git a/src/idmap/sssd b/src/idmap/sssd new file mode 160000 index 0000000..85b17ac --- /dev/null +++ b/src/idmap/sssd @@ -0,0 +1 @@ +Subproject commit 85b17ac21c1b72e57f7c2b5f017f16da8b4b77ae diff --git a/src/idmap/wrapper.h b/src/idmap/wrapper.h new file mode 100644 index 0000000..6be1d21 --- /dev/null +++ b/src/idmap/wrapper.h @@ -0,0 +1 @@ +#include "sssd/src/lib/idmap/sss_idmap.h" From efad733460b68d057d8bc0cbe6fbcc9af02ab2a8 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 16 May 2024 09:53:06 -0600 Subject: [PATCH 03/10] Ensure duplicate providers are not started This has been an issue for a while, but we always picked the first one in the list, so it didn't matter, it seems. Now it is relevant, because the duplicate provider is causing the SSSD idmapping to error due to overlapping ranges. Signed-off-by: David Mulder --- src/common/src/config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/src/config.rs b/src/common/src/config.rs index 9a49b0b..1dab439 100644 --- a/src/common/src/config.rs +++ b/src/common/src/config.rs @@ -450,6 +450,8 @@ impl HimmelblauConfig { let mut sections = self.config.sections(); sections.retain(|s| s != "global"); domains.extend(sections); + domains.sort(); + domains.dedup(); domains } From e38f3c183fe3644d35fb611a65b48c3e6089d812 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 16 May 2024 10:18:19 -0600 Subject: [PATCH 04/10] Test the new and legacy idmapping Signed-off-by: David Mulder --- src/idmap/src/lib.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/idmap/src/lib.rs b/src/idmap/src/lib.rs index 2fa4015..c711ec4 100644 --- a/src/idmap/src/lib.rs +++ b/src/idmap/src/lib.rs @@ -239,3 +239,79 @@ impl Drop for SssIdmap { } } } + +#[cfg(test)] +mod tests { + use crate::{SssIdmap, DEFAULT_IDMAP_RANGE}; + use std::collections::HashMap; + use uuid::Uuid; + + #[test] + fn sssd_idmapping() { + let domain = "contoso.onmicrosoft.com"; + let tenant_id = "d7af6c1b-0497-40fe-9d17-07e6b0f8332e"; + let mut idmap = SssIdmap::new().expect("SssIdmap initialization failed"); + + idmap + .add_gen_domain(domain, tenant_id, DEFAULT_IDMAP_RANGE) + .expect("Failed initializing test domain idmapping"); + + // Verify we always get the same mapping for various users + let mut usermap: HashMap = HashMap::new(); + usermap.insert("tux@contoso.onmicrosoft.com".to_string(), 1912749799); + usermap.insert("admin@contoso.onmicrosoft.com".to_string(), 297515919); + usermap.insert("dave@contoso.onmicrosoft.com".to_string(), 132631922); + usermap.insert("joe@contoso.onmicrosoft.com".to_string(), 361591965); + usermap.insert("georg@contoso.onmicrosoft.com".to_string(), 866887005); + + for (username, expected_uid) in &usermap { + let uid = idmap + .gen_to_unix(tenant_id, username) + .expect(&format!("Failed converting username {} to uid", username)); + assert_eq!(uid, *expected_uid, "Uid for {} did not match", username); + } + } + + #[test] + fn legacy_idmapping() { + let domain = "contoso.onmicrosoft.com"; + let tenant_id = "d7af6c1b-0497-40fe-9d17-07e6b0f8332e"; + let mut idmap = SssIdmap::new().expect("SssIdmap initialization failed"); + + // Test using the legacy default idmap range + idmap + .add_gen_domain(domain, tenant_id, (1000000, 6999999)) + .expect("Failed initializing test domain idmapping"); + + // Verify we always get the same mapping for various users + let mut usermap: HashMap = HashMap::new(); + usermap.insert( + "tux@contoso.onmicrosoft.com".to_string(), + (5627207, "cd4ebec9-434c-4bad-af7c-9c39a4127551".to_string()), + ); + usermap.insert( + "admin@contoso.onmicrosoft.com".to_string(), + (5290834, "4210d86f-ce97-4aff-97f7-bd3789727903".to_string()), + ); + usermap.insert( + "dave@contoso.onmicrosoft.com".to_string(), + (4845027, "97bfcfc4-fb12-445e-aaca-28c6b5375855".to_string()), + ); + usermap.insert( + "joe@contoso.onmicrosoft.com".to_string(), + (3215932, "1e26150d-efe0-4551-b9d3-49ea287c80a7".to_string()), + ); + usermap.insert( + "georg@contoso.onmicrosoft.com".to_string(), + (4966353, "8193af72-71e1-4689-a4ea-b9a05f2639c9".to_string()), + ); + + for (username, (expected_uid, object_id)) in &usermap { + let object_uuid = Uuid::parse_str(&object_id).expect("Failed parsing object_id"); + let uid = idmap + .object_id_to_unix_id(tenant_id, &object_uuid) + .expect(&format!("Failed converting uuid {} to uid", object_id)); + assert_eq!(uid, *expected_uid, "Uid for {} did not match", username); + } + } +} From aebd9d5039823c0ceb905348379dda523ae6cd57 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 16 May 2024 10:30:51 -0600 Subject: [PATCH 05/10] Add CI job for cargo test Signed-off-by: David Mulder --- .github/workflows/test.yml | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1ea616e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,45 @@ +--- +name: Test + +# Trigger the workflow on push or pull request +"on": + pull_request: + branches: + - main + - stable-0.1.x + - stable-0.2.x + - stable-0.3.x + +env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup sccache + uses: mozilla-actions/sccache-action@v0.0.4 + with: + version: "v0.4.2" + - name: Install dependencies + run: | + sudo apt-get update && \ + sudo apt-get install -y \ + libpam0g-dev \ + libudev-dev \ + libssl-dev \ + pkg-config \ + tpm-udev \ + libtss2-dev + + - name: "Fetch submodules" + run: git submodule init && git submodule update + + - name: "Run tests" + run: cargo test + continue-on-error: false From 5b1927cbf19db1f366a4d04a77f5fc9b495685d2 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 16 May 2024 10:40:21 -0600 Subject: [PATCH 06/10] Modify CI workflows to handle idmap build Signed-off-by: David Mulder --- .github/workflows/build.yml | 12 +++++++++++- .github/workflows/clippy.yml | 12 +++++++++++- .github/workflows/test.yml | 12 +++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f2e0881..1ae4c90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,17 @@ jobs: libssl-dev \ pkg-config \ tpm-udev \ - libtss2-dev + libtss2-dev \ + libcap-dev \ + libtalloc-dev \ + libtevent-dev \ + libldb-dev \ + libdhash-dev \ + libkrb5-dev \ + libpcre2-dev \ + libclang-13-dev \ + autoconf \ + gettext - name: "Fetch submodules" run: git submodule init && git submodule update diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 2d866a6..1bdb1fa 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -35,7 +35,17 @@ jobs: libssl-dev \ pkg-config \ tpm-udev \ - libtss2-dev + libtss2-dev \ + libcap-dev \ + libtalloc-dev \ + libtevent-dev \ + libldb-dev \ + libdhash-dev \ + libkrb5-dev \ + libpcre2-dev \ + libclang-13-dev \ + autoconf \ + gettext - name: "Fetch submodules" run: git submodule init && git submodule update diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ea616e..4bfa4d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,17 @@ jobs: libssl-dev \ pkg-config \ tpm-udev \ - libtss2-dev + libtss2-dev \ + libcap-dev \ + libtalloc-dev \ + libtevent-dev \ + libldb-dev \ + libdhash-dev \ + libkrb5-dev \ + libpcre2-dev \ + libclang-13-dev \ + autoconf \ + gettext - name: "Fetch submodules" run: git submodule init && git submodule update From c15f18d09302ead6c02393022a768ff39881fe65 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 16 May 2024 10:51:23 -0600 Subject: [PATCH 07/10] Update Kanidm tracking This fixes some issues with PAM prompts, as well as resolving some clippy warnings. Signed-off-by: David Mulder --- src/kanidm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanidm b/src/kanidm index 540de97..ac9a90a 160000 --- a/src/kanidm +++ b/src/kanidm @@ -1 +1 @@ -Subproject commit 540de971ad7e4430e3ca343dae746de201f95601 +Subproject commit ac9a90abf3a798a60dc09e326d0afecf4e95d6c8 From bf6edfca94aa0a6e595947bac56ff4524cb416ee Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 16 May 2024 10:52:59 -0600 Subject: [PATCH 08/10] Don't stop an MR based on a clippy warning These warning are often found in Kanidm code, which I don't directly control. Best if these warnings don't block an MR. Signed-off-by: David Mulder --- .github/workflows/clippy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 1bdb1fa..4bc5206 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -52,4 +52,4 @@ jobs: - name: "Run clippy" run: cargo clippy --all-features - continue-on-error: false + continue-on-error: true From cd3371c9f51801ab7a6e185cae6a7c216e664c3e Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 16 May 2024 11:17:00 -0600 Subject: [PATCH 09/10] Remove the initial uid hack, use name mapping Rather than providing a hacky fake uid to satisfy OpenSSH, eliminate this and just use upn idmapping here. If Uuid id mapping is enabled, then just bail out. We can set a stipulation that SSH only works with upn/name id mapping. Signed-off-by: David Mulder --- src/common/src/idprovider/himmelblau.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/common/src/idprovider/himmelblau.rs b/src/common/src/idprovider/himmelblau.rs index fd3277b..faea2c4 100644 --- a/src/common/src/idprovider/himmelblau.rs +++ b/src/common/src/idprovider/himmelblau.rs @@ -526,21 +526,36 @@ impl IdProvider for HimmelblauProvider { IdpError::BadRequest })?; if exists { - // Generate a UserToken, with invalid uuid and gid. We can - // only fetch these from an authenticated token. We have to - // provide something, or SSH will fail. + // Generate a UserToken, with invalid uuid. We can + // only fetch this from an authenticated token. + let config = self.config.read().await; + let gidnumber = match config.get_id_attr_map() { + // If Uuid mapping is enabled, bail out now. + // We can only provide a valid idmapping with + // name idmapping at this point. + IdAttr::Uuid => return Err(IdpError::BadRequest), + IdAttr::Name => { + let idmap = self.idmap.read().await; + idmap.gen_to_unix(&self.tenant_id, &account_id).map_err( + |e| { + error!("{:?}", e); + IdpError::BadRequest + }, + )? + } + }; let groups = vec![GroupToken { name: account_id.clone(), spn: account_id.clone(), uuid: Uuid::max(), - gidnumber: i32::MAX as u32, + gidnumber, }]; let config = self.config.read().await; return Ok(UserToken { name: account_id.clone(), spn: account_id.clone(), uuid: Uuid::max(), - gidnumber: i32::MAX as u32, + gidnumber, displayname: "".to_string(), shell: Some(config.get_shell(Some(&self.domain))), groups, From 47549c549c0b80fa2554d89fe3f9e273b7fbddfb Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 16 May 2024 11:32:40 -0600 Subject: [PATCH 10/10] Fix clippy warning about inefficient use of clone() Signed-off-by: David Mulder --- src/common/src/idprovider/himmelblau.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/src/idprovider/himmelblau.rs b/src/common/src/idprovider/himmelblau.rs index faea2c4..2d0dffe 100644 --- a/src/common/src/idprovider/himmelblau.rs +++ b/src/common/src/idprovider/himmelblau.rs @@ -626,7 +626,7 @@ impl IdProvider for HimmelblauProvider { * provide this during a silent acquire */ if let Some(old_token) = old_token { - token.displayname = old_token.displayname.clone() + token.displayname.clone_from(&old_token.displayname) } Ok(token) }