From 1ed4d7c1bdb65a3b810b9954a06708b8ef53dcea Mon Sep 17 00:00:00 2001 From: Firstyear Date: Thu, 10 Nov 2022 07:43:22 +1000 Subject: [PATCH] 20221103 ipa import driver (#1180) --- Cargo.lock | 122 ++++-- Cargo.toml | 13 +- iam_migrations/freeipa/00config-mod.ldif | 2 +- iam_migrations/freeipa/Cargo.toml | 36 ++ iam_migrations/freeipa/src/config.rs | 12 + iam_migrations/freeipa/src/main.rs | 507 +++++++++++++++++++++++ iam_migrations/freeipa/src/opt.rs | 31 ++ iam_migrations/freeipa/src/tests.rs | 423 +++++++++++++++++++ kanidm_client/src/lib.rs | 1 + kanidm_client/src/scim.rs | 16 + kanidm_client/src/sync_account.rs | 17 + kanidm_proto/Cargo.toml | 1 + kanidm_proto/src/scim_v1.rs | 150 ++++++- kanidm_tools/Cargo.toml | 2 +- kanidm_tools/src/cli/lib.rs | 3 + kanidm_tools/src/cli/synch.rs | 68 +++ kanidm_tools/src/opt/kanidm.rs | 41 ++ kanidmd/core/src/actors/v1_read.rs | 2 +- kanidmd/core/src/actors/v1_scim.rs | 50 ++- kanidmd/core/src/https/mod.rs | 5 +- kanidmd/core/src/https/v1_scim.rs | 45 +- kanidmd/lib/src/constants/acp.rs | 3 +- kanidmd/lib/src/entry.rs | 6 +- kanidmd/lib/src/idm/scim.rs | 174 +++++++- kanidmd/lib/src/ldap.rs | 18 +- kanidmd/lib/src/server.rs | 11 +- orca/src/ds.rs | 85 ++-- 27 files changed, 1721 insertions(+), 123 deletions(-) create mode 100644 iam_migrations/freeipa/Cargo.toml create mode 100644 iam_migrations/freeipa/src/config.rs create mode 100644 iam_migrations/freeipa/src/main.rs create mode 100644 iam_migrations/freeipa/src/opt.rs create mode 100644 iam_migrations/freeipa/src/tests.rs create mode 100644 kanidm_client/src/scim.rs create mode 100644 kanidm_tools/src/cli/synch.rs diff --git a/Cargo.lock b/Cargo.lock index 4b438d953..5fd564313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,7 +134,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.16", + "time 0.3.17", ] [[package]] @@ -596,9 +596,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aec14f5d4e6e3f927cd0c81f72e5710d95ee9019fbeb4b3021193867491bfd8" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" [[package]] name = "byteorder" @@ -769,9 +769,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "compact_jwt" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5656b98b1584764a52906e67caec20dfb9b0179ac2052d0d5937b083bc39a120" +checksum = "51f9032b96a89dd79ffc5f62523d5351ebb40680cbdfc4029393b511b9e971aa" dependencies = [ "base64 0.13.1", "base64urlsafedata", @@ -869,7 +869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" dependencies = [ "percent-encoding", - "time 0.3.16", + "time 0.3.17", "version_check", ] @@ -885,7 +885,7 @@ dependencies = [ "publicsuffix", "serde", "serde_json", - "time 0.3.16", + "time 0.3.17", "url", ] @@ -2193,9 +2193,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" [[package]] name = "itertools" @@ -2245,6 +2245,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kanidm-ipa-sync" +version = "1.1.0-alpha.11-dev" +dependencies = [ + "base64urlsafedata", + "clap", + "clap_complete", + "kanidm_client", + "kanidm_proto", + "kanidmd_lib", + "ldap3_client", + "serde", + "serde_json", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "url", + "users", +] + [[package]] name = "kanidm_client" version = "1.1.0-alpha.11-dev" @@ -2269,6 +2290,7 @@ dependencies = [ "base32", "base64urlsafedata", "last-git-commit", + "scim_proto", "serde", "serde_json", "time 0.2.27", @@ -2518,16 +2540,36 @@ dependencies = [ "nom 2.2.1", ] +[[package]] +name = "ldap3_client" +version = "0.3.0" +source = "git+https://github.com/kanidm/ldap3.git#6b0d146d3f85a32add3bdc3639ba4146822eb861" +dependencies = [ + "base64 0.13.1", + "base64urlsafedata", + "futures-util", + "ldap3_proto", + "openssl", + "serde", + "tokio", + "tokio-openssl", + "tokio-util", + "tracing", + "url", + "uuid", +] + [[package]] name = "ldap3_proto" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d7f04b6dc4d5401b817596e424ecb4a0931db1418f3987a27e0ab69320665e" +version = "0.3.0" +source = "git+https://github.com/kanidm/ldap3.git#6b0d146d3f85a32add3bdc3639ba4146822eb861" dependencies = [ "bytes", "lber", + "nom 7.1.1", "tokio-util", "tracing", + "uuid", ] [[package]] @@ -2768,9 +2810,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", @@ -2886,9 +2928,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ "hermit-abi", "libc", @@ -2915,15 +2957,6 @@ dependencies = [ "syn", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "oauth2" version = "4.2.3" @@ -3256,9 +3289,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" @@ -3539,9 +3572,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -3747,6 +3780,21 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "scim_proto" +version = "0.1.0" +source = "git+https://github.com/kanidm/scim.git#f7a9241bf413ac2e40cb974876d2b0433a866c74" +dependencies = [ + "base64urlsafedata", + "serde", + "serde_json", + "time 0.2.27", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -4411,16 +4459,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa 1.0.4", - "libc", - "num_threads", "serde", "time-core", - "time-macros 0.2.5", + "time-macros 0.2.6", ] [[package]] @@ -4441,9 +4487,9 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" dependencies = [ "time-core", ] @@ -5250,7 +5296,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.16", + "time 0.3.17", ] [[package]] @@ -5370,5 +5416,5 @@ dependencies = [ "lazy_static", "quick-error", "regex", - "time 0.3.16", + "time 0.3.17", ] diff --git a/Cargo.toml b/Cargo.toml index 678fbccb8..2c9aae0a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ lto = "thin" [workspace] members = [ + "iam_migrations/freeipa", "kanidm_client", "kanidm_proto", "kanidm_tools", @@ -83,7 +84,17 @@ kanidm_unix_int = { path = "./kanidm_unix_int" } last-git-commit = "0.2.0" # REMOVE this lazy_static = "^1.4.0" -ldap3_proto = "^0.2.3" +# ldap3_client = "^0.3.0" +# ldap3_proto = "^0.3.0" + +# ldap3_client = { path = "../ldap3/client", version = "0.3.0" } +# ldap3_proto = { path = "../ldap3/proto", version = "0.3.0" } +# scim_proto = { path = "../scim/proto", version = "0.1.0" } + +ldap3_client = { git = "https://github.com/kanidm/ldap3.git", version = "0.3.0" } +ldap3_proto = { git = "https://github.com/kanidm/ldap3.git", version = "0.3.0" } +scim_proto = { git = "https://github.com/kanidm/scim.git", version = "0.1.0" } + libc = "^0.2.135" libnss = "^0.4.0" libsqlite3-sys = "^0.25.0" diff --git a/iam_migrations/freeipa/00config-mod.ldif b/iam_migrations/freeipa/00config-mod.ldif index 0b16836e9..bf7b17f46 100644 --- a/iam_migrations/freeipa/00config-mod.ldif +++ b/iam_migrations/freeipa/00config-mod.ldif @@ -1,4 +1,4 @@ dn: cn=Retro Changelog Plugin,cn=plugins,cn=config changetype: modify add: nsslapd-include-suffix -nsslapd-include-suffix: cn=accounts,dc=dev,dc=kanidm,dc=com +nsslapd-include-suffix: dc=dev,dc=kanidm,dc=com diff --git a/iam_migrations/freeipa/Cargo.toml b/iam_migrations/freeipa/Cargo.toml new file mode 100644 index 000000000..a8c3beec0 --- /dev/null +++ b/iam_migrations/freeipa/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "kanidm-ipa-sync" +description = "Kanidm Client Tools" +documentation = "https://kanidm.github.io/kanidm/stable/" + +version.workspace = true +authors.workspace = true +rust-version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +base64urlsafedata.workspace = true +clap = { workspace = true, features = ["derive", "env"] } +kanidm_client.workspace = true +kanidm_proto.workspace = true +tokio = { workspace = true, features = ["rt", "macros"] } +tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } + +users.workspace = true + +ldap3_client.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +toml.workspace = true +url = { workspace = true, features = ["serde"] } + +# For file metadata, should this me moved out? +kanidmd_lib.workspace = true + +[build-dependencies] +clap = { workspace = true, features = ["derive"] } +clap_complete.workspace = true diff --git a/iam_migrations/freeipa/src/config.rs b/iam_migrations/freeipa/src/config.rs new file mode 100644 index 000000000..8c9f65aa2 --- /dev/null +++ b/iam_migrations/freeipa/src/config.rs @@ -0,0 +1,12 @@ +use serde::Deserialize; +use url::Url; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub sync_token: String, + pub ipa_uri: Url, + pub ipa_ca: String, + pub ipa_sync_dn: String, + pub ipa_sync_pw: String, + pub ipa_sync_base_dn: String, +} diff --git a/iam_migrations/freeipa/src/main.rs b/iam_migrations/freeipa/src/main.rs new file mode 100644 index 000000000..00824d094 --- /dev/null +++ b/iam_migrations/freeipa/src/main.rs @@ -0,0 +1,507 @@ +#![deny(warnings)] +#![warn(unused_extern_crates)] +#![deny(clippy::todo)] +#![deny(clippy::unimplemented)] +#![deny(clippy::unwrap_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)] +// We allow expect since it forces good error messages at the least. +#![allow(clippy::expect_used)] + +mod config; + +#[cfg(test)] +mod tests; + +use crate::config::Config; +use clap::Parser; +use std::fs::metadata; +use std::fs::File; +use std::io::Read; +use std::os::unix::fs::MetadataExt; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::thread; +use tokio::runtime; +use tracing::{debug, error, info, warn}; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{fmt, EnvFilter}; + +use kanidm_client::KanidmClientBuilder; +use kanidm_proto::scim_v1::{ + ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, ScimSyncState, +}; +use kanidmd_lib::utils::file_permissions_readonly; + +use users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid}; + +use ldap3_client::{ + proto, proto::LdapFilter, LdapClientBuilder, LdapSyncRepl, LdapSyncReplEntry, + LdapSyncStateValue, +}; + +include!("./opt.rs"); + +async fn driver_main(opt: Opt) { + debug!("Starting kanidm freeipa sync driver."); + // Parse the configs. + + let mut f = match File::open(&opt.ipa_sync_config) { + Ok(f) => f, + Err(e) => { + error!("Unable to open profile file [{:?}] 🥺", e); + return; + } + }; + + let mut contents = String::new(); + if let Err(e) = f.read_to_string(&mut contents) { + error!("unable to read profile contents {:?}", e); + return; + }; + + let sync_config: Config = match toml::from_str(contents.as_str()) { + Ok(c) => c, + Err(e) => { + eprintln!("unable to parse config {:?}", e); + return; + } + }; + + debug!(?sync_config); + + let cb = match KanidmClientBuilder::new().read_options_from_optional_config(&opt.client_config) + { + Ok(v) => v, + Err(_) => { + error!("Failed to parse {}", opt.client_config.to_string_lossy()); + return; + } + }; + + // Do we need this? + // let cb = cb.connect_timeout(cfg.conn_timeout); + + let rsclient = match cb.build() { + Ok(rsc) => rsc, + Err(_e) => { + error!("Failed to build async client"); + return; + } + }; + + rsclient.set_token(sync_config.sync_token.clone()).await; + + // Preflight check. + // * can we connect to ipa? + + let mut ipa_client = match LdapClientBuilder::new(&sync_config.ipa_uri) + .add_tls_ca(&sync_config.ipa_ca) + .build() + .await + { + Ok(lc) => lc, + Err(e) => { + error!(?e, "Failed to connect to freeipa"); + return; + } + }; + + match ipa_client + .bind( + sync_config.ipa_sync_dn.clone(), + sync_config.ipa_sync_pw.clone(), + ) + .await + { + Ok(()) => { + debug!(ipa_sync_dn = ?sync_config.ipa_sync_dn, ipa_uri = %sync_config.ipa_uri); + } + Err(e) => { + error!(?e, "Failed to bind (authenticate) to freeipa"); + return; + } + }; + + // * can we connect to kanidm? + // - get the current sync cookie from kanidm. + let scim_sync_status = match rsclient.scim_v1_sync_status().await { + Ok(s) => s, + Err(e) => { + error!(?e, "Failed to access scim sync status"); + return; + } + }; + + debug!(state=?scim_sync_status); + + // === Everything is connected! === + + // Based on the scim_sync_status, perform our sync repl + + let mode = proto::SyncRequestMode::RefreshOnly; + + let cookie = match &scim_sync_status { + ScimSyncState::Refresh => None, + ScimSyncState::Active { cookie } => Some(cookie.0.clone()), + }; + + let filter = LdapFilter::Or(vec![ + // LdapFilter::Equality("objectclass".to_string(), "domain".to_string()), + LdapFilter::And(vec![ + LdapFilter::Equality("objectclass".to_string(), "person".to_string()), + LdapFilter::Equality("objectclass".to_string(), "ipantuserattrs".to_string()), + LdapFilter::Equality("objectclass".to_string(), "posixaccount".to_string()), + ]), + LdapFilter::And(vec![ + LdapFilter::Equality("objectclass".to_string(), "groupofnames".to_string()), + LdapFilter::Equality("objectclass".to_string(), "ipausergroup".to_string()), + LdapFilter::Not(Box::new(LdapFilter::Equality( + "objectclass".to_string(), + "mepmanagedentry".to_string(), + ))), + // Need to exclude the admins group as it gid conflicts to admin. + LdapFilter::Not(Box::new(LdapFilter::Equality( + "cn".to_string(), + "admins".to_string(), + ))), + // Kani internally has an all persons group. + LdapFilter::Not(Box::new(LdapFilter::Equality( + "cn".to_string(), + "ipausers".to_string(), + ))), + ]), + LdapFilter::And(vec![ + LdapFilter::Equality("objectclass".to_string(), "ipatoken".to_string()), + LdapFilter::Equality("objectclass".to_string(), "ipatokentotp".to_string()), + ]), + ]); + + debug!(ipa_sync_base_dn = ?sync_config.ipa_sync_base_dn, ?cookie, ?mode, ?filter); + let sync_result = match ipa_client + .syncrepl(sync_config.ipa_sync_base_dn, filter, cookie, mode) + .await + { + Ok(results) => results, + Err(e) => { + error!(?e, "Failed to perform syncrepl from ipa"); + return; + } + }; + + if opt.proto_dump { + let stdout = std::io::stdout(); + if let Err(e) = serde_json::to_writer_pretty(stdout, &sync_result) { + error!(?e, "Failed to serialise ldap sync response"); + } + } + + // pre-process the entries. + // - > fn so we can test. + let scim_sync_request = match process_ipa_sync_result(scim_sync_status, sync_result).await { + Ok(ssr) => ssr, + Err(()) => return, + }; + + if opt.proto_dump { + let stdout = std::io::stdout(); + // write it out. + if let Err(e) = serde_json::to_writer_pretty(stdout, &scim_sync_request) { + error!(?e, "Failed to serialise scim sync request"); + }; + } else { + if let Err(e) = rsclient.scim_v1_sync_update(&scim_sync_request).await { + error!( + ?e, + "Failed to submit scim sync update - see the kanidmd server log for more details." + ); + }; + } + // done! +} + +async fn process_ipa_sync_result( + from_state: ScimSyncState, + sync_result: LdapSyncRepl, +) -> Result { + match sync_result { + LdapSyncRepl::Success { + cookie, + refresh_deletes, + entries, + delete_uuids, + present_uuids, + } => { + if refresh_deletes { + error!("Unsure how to handle refreshDeletes=True"); + return Err(()); + } + + if !present_uuids.is_empty() { + error!("Unsure how to handle presentUuids > 0"); + return Err(()); + } + + let to_state = cookie + .map(|cookie| { + ScimSyncState::Active { cookie } + }) + .ok_or_else(|| { + error!("Invalid state, ldap sync repl did not provide a valid state cookie in response."); + })?; + + // Future - make this par-map + let entries = entries + .into_iter() + .filter_map(|e| match ipa_to_scim_entry(e) { + Ok(Some(e)) => Some(Ok(e)), + Ok(None) => None, + Err(()) => Some(Err(())), + }) + .collect::, _>>(); + + let entries = match entries { + Ok(e) => e, + Err(()) => { + error!("Failed to process IPA entries to SCIM"); + return Err(()); + } + }; + + Ok(ScimSyncRequest { + from_state, + to_state, + entries, + delete_uuids, + }) + } + LdapSyncRepl::RefreshRequired => { + let to_state = ScimSyncState::Refresh; + + Ok(ScimSyncRequest { + from_state, + to_state, + entries: Vec::new(), + delete_uuids: Vec::new(), + }) + } + } +} + +fn ipa_to_scim_entry(sync_entry: LdapSyncReplEntry) -> Result, ()> { + debug!("{:#?}", sync_entry); + // Is this an entry we need to observe/look at? + + // check the sync_entry state? + if sync_entry.state != LdapSyncStateValue::Add { + todo!(); + } + + let dn = sync_entry.entry.dn.clone(); + + let oc = sync_entry.entry.attrs.get("objectclass").ok_or_else(|| { + error!("Invalid entry - no object class {}", dn); + })?; + + if oc.contains("person") { + let LdapSyncReplEntry { + entry_uuid, + state: _, + mut entry, + } = sync_entry; + + let id = entry_uuid; + + let user_name = entry.remove_ava_single("uid").ok_or_else(|| { + error!("Missing required attribute uid"); + })?; + + let display_name = entry.remove_ava_single("cn").ok_or_else(|| { + error!("Missing required attribute cn"); + })?; + + let gidnumber = entry + .remove_ava_single("gidnumber") + .map(|gid| { + u32::from_str(&gid).map_err(|_| { + error!("Invalid gidnumber"); + }) + }) + .transpose()?; + + let homedirectory = entry.remove_ava_single("homedirectory"); + let password_import = entry.remove_ava_single("ipanthash"); + let login_shell = entry.remove_ava_single("loginshell"); + let external_id = Some(entry.dn); + + Ok(Some( + ScimSyncPerson { + id, + external_id, + user_name, + display_name, + gidnumber, + homedirectory, + password_import, + login_shell, + } + .into(), + )) + } else if oc.contains("groupofnames") { + let LdapSyncReplEntry { + entry_uuid, + state: _, + mut entry, + } = sync_entry; + + let id = entry_uuid; + + let name = entry.remove_ava_single("cn").ok_or_else(|| { + error!("Missing required attribute cn"); + })?; + + let description = entry.remove_ava_single("description"); + + let gidnumber = entry + .remove_ava_single("gidnumber") + .map(|gid| { + u32::from_str(&gid).map_err(|_| { + error!("Invalid gidnumber"); + }) + }) + .transpose()?; + + let members: Vec<_> = entry + .remove_ava("member") + .map(|set| { + set.into_iter() + .map(|external_id| ScimExternalMember { external_id }) + .collect() + }) + .unwrap_or_default(); + + let external_id = Some(entry.dn); + + Ok(Some( + ScimSyncGroup { + id, + external_id, + name, + description, + gidnumber, + members, + } + .into(), + )) + } else if oc.contains("ipatokentotp") { + // Skip for now, we don't supporty multiple totp yet. + Ok(None) + } else { + debug!("Skipping entry {} with oc {:?}", dn, oc); + Ok(None) + } +} + +fn config_security_checks(cfg_path: &Path) -> bool { + let cfg_path_str = cfg_path.to_string_lossy(); + + if !cfg_path.exists() { + // there's no point trying to start up if we can't read a usable config! + error!( + "Config missing from {} - cannot start up. Quitting.", + cfg_path_str + ); + return false; + } else { + let cfg_meta = match metadata(&cfg_path) { + Ok(v) => v, + Err(e) => { + error!("Unable to read metadata for {} - {:?}", cfg_path_str, e); + return false; + } + }; + if !file_permissions_readonly(&cfg_meta) { + warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", + cfg_path_str + ); + } + + let cuid = get_current_uid(); + let ceuid = get_effective_uid(); + + if cfg_meta.uid() == cuid || cfg_meta.uid() == ceuid { + warn!("WARNING: {} owned by the current uid, which may allow file permission changes. This could be a security risk ...", + cfg_path_str + ); + } + + true + } +} + +fn main() { + let cuid = get_current_uid(); + let ceuid = get_effective_uid(); + let cgid = get_current_gid(); + let cegid = get_effective_gid(); + + let opt = Opt::parse(); + + let fmt_layer = fmt::layer().with_writer(std::io::stderr); + + let filter_layer = if opt.debug { + match EnvFilter::try_new("kanidm_client=debug,kanidm_ipa_sync=debug,ldap3_client=debug") { + Ok(f) => f, + Err(e) => { + eprintln!("ERROR! Unable to start tracing {:?}", e); + return; + } + } + } else { + match EnvFilter::try_from_default_env() { + Ok(f) => f, + Err(_) => EnvFilter::new("kanidm_client=warn,kanidm_ipa_sync=info,ldap3_client=warn"), + } + }; + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .init(); + + // Startup sanity checks. + if opt.skip_root_check { + warn!("Skipping root user check, if you're running this for testing, ensure you clean up temporary files.") + // TODO: this wording is not great m'kay. + } else { + if cuid == 0 || ceuid == 0 || cgid == 0 || cegid == 0 { + error!("Refusing to run - this process must not operate as root."); + return; + } + }; + + if !config_security_checks(&opt.client_config) || !config_security_checks(&opt.ipa_sync_config) + { + return; + } + + let par_count = thread::available_parallelism() + .expect("Failed to determine available parallelism") + .get(); + + let rt = runtime::Builder::new_current_thread() + // We configure this as we use parallel workers at some points. + .max_blocking_threads(par_count) + .enable_all() + .build() + .expect("Failed to initialise tokio runtime!"); + + tracing::debug!("Using {} worker threads", par_count); + + rt.block_on(async move { driver_main(opt).await }); + + info!("Success!"); +} diff --git a/iam_migrations/freeipa/src/opt.rs b/iam_migrations/freeipa/src/opt.rs new file mode 100644 index 000000000..80500c1bd --- /dev/null +++ b/iam_migrations/freeipa/src/opt.rs @@ -0,0 +1,31 @@ + + +use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH; +pub const DEFAULT_IPA_CONFIG_PATH: &str = "/etc/kanidm/ipa-sync"; + + +#[derive(Debug, clap::Parser)] +#[clap(about = "Kanidm FreeIPA Sync Driver")] +pub struct Opt { + /// Enable debbuging of the sync driver + #[clap(short, long, env = "KANIDM_DEBUG")] + pub debug: bool, + /// Path to the client config file. + #[clap(parse(from_os_str), short, long, default_value_os_t = DEFAULT_CLIENT_CONFIG_PATH.into())] + pub client_config: PathBuf, + + /// Path to the ipa-sync config file. + #[clap(parse(from_os_str), short, long, default_value_os_t = DEFAULT_IPA_CONFIG_PATH.into())] + pub ipa_sync_config: PathBuf, + + #[clap(short, long, hide = true)] + /// Dump the ldap protocol inputs, as well as the scim outputs. This can be used + /// to create test cases for testing the parser. + /// + /// No actions are taken on the kanidm instance, this is purely a dump of the + /// state in/out. + pub proto_dump: bool, + + #[clap(short, long, hide = true)] + pub skip_root_check: bool, +} diff --git a/iam_migrations/freeipa/src/tests.rs b/iam_migrations/freeipa/src/tests.rs new file mode 100644 index 000000000..211f11dfb --- /dev/null +++ b/iam_migrations/freeipa/src/tests.rs @@ -0,0 +1,423 @@ +use crate::process_ipa_sync_result; +use kanidm_proto::scim_v1::ScimSyncState; + +use ldap3_client::LdapSyncRepl; + +#[tokio::test] +async fn test_ldap_to_scim() { + let _ = tracing_subscriber::fmt::try_init(); + + let sync_request: LdapSyncRepl = + serde_json::from_str(TEST_LDAP_SYNC_REPL_1).expect("failed to parse ldap sync"); + + let scim_sync_request = process_ipa_sync_result(ScimSyncState::Refresh, sync_request) + .await + .expect("failed to process ldap sync repl"); + + assert!(matches!( + scim_sync_request.from_state, + ScimSyncState::Refresh + )); + + // need to setup a fake ldap sync result. + + // What do we expect? +} + +const TEST_LDAP_SYNC_REPL_1: &str = r#" +{ + "Success": { + "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzEwOQ", + "refresh_deletes": false, + "entries": [ + { + "entry_uuid": "ac60034b-3498-11ed-a50d-919b4b1a5ec0", + "state": "Add", + "entry": { + "dn": "uid=admin,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "attrs": { + "cn": [ + "Administrator" + ], + "gecos": [ + "Administrator" + ], + "gidnumber": [ + "8200000" + ], + "homedirectory": [ + "/home/admin" + ], + "ipanthash": [ + "CVBguEizG80swI8sftaknw" + ], + "ipantsecurityidentifier": [ + "S-1-5-21-148961183-2750130983-218252910-500" + ], + "ipauniqueid": [ + "ad15f644-3498-11ed-95c3-5254006b0418" + ], + "krbextradata": [ + "AAL4hSJjcm9vdC9hZG1pbkBERVYuQkxBQ0tIQVRTLk5FVC5BVQA" + ], + "krblastadminunlock": [ + "20220915015504Z" + ], + "krblastfailedauth": [ + "20221108050316Z" + ], + "krblastpwdchange": [ + "20220915015504Z" + ], + "krbloginfailedcount": [ + "0" + ], + "krbpasswordexpiration": [ + "20221214015504Z" + ], + "krbprincipalkey": [ + "MIIB1KADAgEBoQMCAQGiAwIBAaMDAgEBpIIBvDCCAbgwdKAbMBmgAwIBBKESBBBgeEMvRkhoVWphRX0iKXxCoVUwU6ADAgEUoUwESiAAuyt8szEUVLiWVjSTuUgbgCf8heFMeIhSmGTgJpwL50kddprbdeKuOYvyxepdAil/MqHs4qdqj54reDDqFW0T2bg1Iv9O1cZEMGSgGzAZoAMCAQShEgQQU2xOXT16V21hPFkzPClsJKFFMEOgAwIBE6E8BDoQALfdG+243xBQDt01+bFr46DcZnlHctoSyUQKw8I8FzvRE1LK9Ttl5qkkOHADpA7XSj1lQ2RFqBsSMHSgGzAZoAMCAQShEgQQay9XSC9tPDJJVjIwUDxFRKFVMFOgAwIBEqFMBEogADJjxICRFFzpOcsxMY3xVedF3IBd7qzsQJlSvShaeKwyhTBFI/wvVDtQq6ogWKlACUcAVk2N6p91VtRHHjxXVhKQvT0kt/KS7zBkoBswGaADAgEEoRIEEE5nNTh5SmgpZic0bDAmNUWhRTBDoAMCARGhPAQ6EAClGqBf9jZWixZo/evVMVH01NkI1VpR0fNrGyvtML78p5j6TAne5Nms/wj9BtVawuv+h+Gz1fjdfw" + ], + "krbprincipalname": [ + "admin@DEV.BLACKHATS.NET.AU", + "root@DEV.BLACKHATS.NET.AU" + ], + "loginshell": [ + "/bin/bash" + ], + "memberof": [ + "cn=Add Configuration Sub-Entries,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Add Replication Agreements,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Host Enrollment,cn=privileges,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Modify DNA Range,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Modify PassSync Managers Configuration,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Modify Replication Agreements,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Read DNA Range,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Read LDBM Database Configuration,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Read PassSync Managers Configuration,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Read Replication Agreements,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Read Replication Changelog Configuration,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Remove Replication Agreements,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Replication Administrators,cn=privileges,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=System: Add krbPrincipalName to a Host,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=System: Enroll a Host,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=System: Manage Host Certificates,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=System: Manage Host Enrollment Password,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=System: Manage Host Keytab,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=System: Manage Host Principals,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=Write Replication Changelog Configuration,cn=permissions,cn=pbac,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=admins,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "cn=trust admins,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au" + ], + "objectclass": [ + "inetuser", + "ipantuserattrs", + "ipaobject", + "ipasshgroupofpubkeys", + "ipasshuser", + "krbprincipalaux", + "krbticketpolicyaux", + "person", + "posixaccount", + "top" + ], + "sn": [ + "Administrator" + ], + "uid": [ + "admin" + ], + "uidnumber": [ + "8200000" + ], + "userpassword": [ + "{PBKDF2_SHA256}AAAIAJ3EnyWJXp/ytIk6sqf1BbLO9fzObD3q5I4y2bRFfgAFVo6CaRAaZ7KPYzU6Y340VSUV4NGRRcBjeU8q+aoTOkuzQM91jl+xlCydiB0CjeIDZ0tGy4NmQUFzfg7+exsKhNk2MfUrHcaqfZBtT7Lkfei4Rk7810TQf3NlHIRO8K3egPQ8Ox52Upw1E5QGEKQmDOjrtLtOF5gbyFtR5wc0wUJfmMhd/g65GkqFIr5vbPan3kL3ZqMhh1rrj4ISi9Ui8P7E8GDicoJDPwPf6YD9D0dx6yk72GyiuYt6p2aGJWMY897xqgB+YMgPptiDPik22ExoBAoHeJNIzKjITc2ohLLn6RkCk4GcCwMVZmcxesl/T/OMeSkNvoOM1zy7ANsGbQeaLqpViJSV0xT5PJ6NoIKMU2pIP57Q17VAlYigtCPU" + ] + } + } + }, + { + "entry_uuid": "ac60034e-3498-11ed-a50d-919b4b1a5ec0", + "state": "Add", + "entry": { + "dn": "cn=editors,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "attrs": { + "cn": [ + "editors" + ], + "description": [ + "Limited admins who can edit other users" + ], + "gidnumber": [ + "8200002" + ], + "ipantsecurityidentifier": [ + "S-1-5-21-148961183-2750130983-218252910-1002" + ], + "ipauniqueid": [ + "ad191e00-3498-11ed-b143-5254006b0418" + ], + "objectclass": [ + "groupofnames", + "ipantgroupattrs", + "ipaobject", + "ipausergroup", + "nestedgroup", + "posixgroup", + "top" + ] + } + } + }, + { + "entry_uuid": "0c56a965-3499-11ed-a50d-919b4b1a5ec0", + "state": "Add", + "entry": { + "dn": "cn=trust admins,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "attrs": { + "cn": [ + "trust admins" + ], + "description": [ + "Trusts administrators group" + ], + "ipauniqueid": [ + "0f233c48-3499-11ed-8e23-5254006b0418" + ], + "member": [ + "uid=admin,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au" + ], + "objectclass": [ + "groupofnames", + "ipaobject", + "ipausergroup", + "nestedgroup", + "top" + ] + } + } + }, + { + "entry_uuid": "babb8302-43a1-11ed-a50d-919b4b1a5ec0", + "state": "Add", + "entry": { + "dn": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "attrs": { + "cn": [ + "Test User" + ], + "displayname": [ + "Test User" + ], + "gecos": [ + "Test User" + ], + "gidnumber": [ + "12345" + ], + "givenname": [ + "Test" + ], + "homedirectory": [ + "/home/testuser" + ], + "initials": [ + "TU" + ], + "ipanthash": [ + "iEb36u6PsRetBr3YMLdYbA" + ], + "ipantsecurityidentifier": [ + "S-1-5-21-148961183-2750130983-218252910-1004" + ], + "ipauniqueid": [ + "d939d566-43a1-11ed-85aa-5254006b0418" + ], + "ipauserauthtype": [ + "password" + ], + "krbcanonicalname": [ + "testuser@DEV.BLACKHATS.NET.AU" + ], + "krbextradata": [ + "AAL732ljdGVzdHVzZXJAREVWLkJMQUNLSEFUUy5ORVQuQVUA" + ], + "krblastadminunlock": [ + "20221108044931Z" + ], + "krblastfailedauth": [ + "20221108045207Z" + ], + "krblastpwdchange": [ + "20221108045003Z" + ], + "krbloginfailedcount": [ + "0" + ], + "krbpasswordexpiration": [ + "20230206045003Z" + ], + "krbprincipalkey": [ + "MIIB1KADAgEBoQMCAQGiAwIBBKMDAgEBpIIBvDCCAbgwdKAbMBmgAwIBBKESBBAhIyRjVSl7LkVCXCZqISkkoVUwU6ADAgEUoUwESiAAyMSJYrMnu6mUnDV3ls7arH782SiSi1+vSFosLoLogJZQHKAxUljESwhySlEn+tAEF3yEenvuigNNDtFS/cYMn4oQ1c/vH4tnMGSgGzAZoAMCAQShEgQQOGVcI0Q7YS53OCdxcmd6WaFFMEOgAwIBE6E8BDoQAG6TZ38sFh9gXirZsZcZEiFls92uUh1+Azz7DxrCpo0B8+he39ACvuwLIaxzfswHZE8/pQUFRiHeMHSgGzAZoAMCAQShEgQQOFpvIFxvRlFEVilZVkYhRaFVMFOgAwIBEqFMBEogANBXnuehcaBtCPIXvaGcUEXXkGxiHlDIBFhXeu6l6w0Nj2Cm8Fezun8ip+si3JuxZkaK7TlxccZQOpjxSRuwekeKrzTNp+vS7jBkoBswGaADAgEEoRIEEFZDXFBrSEZDO0kuX2BORyihRTBDoAMCARGhPAQ6EAChj/DZFH3h9pW31ipzT4PrtdDcR83qla52bf+bLDV6LFV6FvFqq3fBJnpiIwuD9rPBBuDut+1ncg" + ], + "krbprincipalname": [ + "testuser@DEV.BLACKHATS.NET.AU" + ], + "loginshell": [ + "/bin/sh" + ], + "mail": [ + "testuser@dev.blackhats.net.au" + ], + "memberof": [ + "cn=ipausers,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au" + ], + "mepmanagedentry": [ + "cn=testuser,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au" + ], + "objectclass": [ + "inetorgperson", + "inetuser", + "ipantuserattrs", + "ipaobject", + "ipasshgroupofpubkeys", + "ipasshuser", + "ipauserauthtypeclass", + "krbprincipalaux", + "krbticketpolicyaux", + "meporiginentry", + "organizationalperson", + "person", + "posixaccount", + "top" + ], + "sn": [ + "User" + ], + "uid": [ + "testuser" + ], + "uidnumber": [ + "8200004" + ], + "userpassword": [ + "{PBKDF2_SHA256}AAAIAOTKJTaS7zR1u0ar5vDHPzcd9FoDiQVYvpT/n19NpTQJKJfdugke9vwpYxaZk+SnR/WHi4oeKd1IyaVmAC+H5d4qUYcc74xLGoyaezCNy8HkKBz9Q/9MD/gvzUjWUTYjbnXAMjzVpAHhVtzAoPrZVoWgXWkhga+YDsqKnqG0g1UeMTgja2zYr0mrG6Y+w+VJP3nnbQ9q4vpb7MGIs8xgjse+nIWZC+mPrK4ZEjSeE9Tjj+0C6nFq1+xU6KZK8NOG8kuHyVeS87zddJApqLSb2p6X/ixobak1j8VzXFd9lxewMfY+gieoXtn47KCFsquWGlavY4ZqjHYu4+MuHDTN/s8E06O/DkLLxPPO4iSH1B6pIaVTMHxsybX7FRLTj/MOb2+oYwWZty8WJ+dRD7gDg0vdUJr/H8EzJkrdXhNyz7f+" + ] + } + } + }, + { + "entry_uuid": "f4dbef82-5f20-11ed-a50d-919b4b1a5ec0", + "state": "Add", + "entry": { + "dn": "ipatokenuniqueid=380e27a4-438d-4c94-9dde-a3f6bc64ea1a,cn=otp,dc=dev,dc=blackhats,dc=net,dc=au", + "attrs": { + "ipatokenotpalgorithm": [ + "sha1" + ], + "ipatokenotpdigits": [ + "6" + ], + "ipatokenotpkey": [ + "hS/2a0DXBSKMJIlIcAJbBfCCZN0b8aTpoaJcj0RAAlV7QRQ" + ], + "ipatokenowner": [ + "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au" + ], + "ipatokentotpclockoffset": [ + "0" + ], + "ipatokentotptimestep": [ + "30" + ], + "ipatokenuniqueid": [ + "380e27a4-438d-4c94-9dde-a3f6bc64ea1a" + ], + "objectclass": [ + "ipatoken", + "ipatokentotp", + "top" + ] + } + } + }, + { + "entry_uuid": "d547c581-5f26-11ed-a50d-919b4b1a5ec0", + "state": "Add", + "entry": { + "dn": "cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "attrs": { + "cn": [ + "testgroup" + ], + "description": [ + "Test group" + ], + "ipauniqueid": [ + "f1b96e6c-5f26-11ed-8cd2-5254006b0418" + ], + "objectclass": [ + "groupofnames", + "ipaobject", + "ipausergroup", + "nestedgroup", + "top" + ] + } + } + }, + { + "entry_uuid": "d547c583-5f26-11ed-a50d-919b4b1a5ec0", + "state": "Add", + "entry": { + "dn": "cn=testexternal,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "attrs": { + "cn": [ + "testexternal" + ], + "ipauniqueid": [ + "f67fd292-5f26-11ed-a6d0-5254006b0418" + ], + "objectclass": [ + "groupofnames", + "ipaexternalgroup", + "ipaobject", + "ipausergroup", + "nestedgroup", + "top" + ] + } + } + }, + { + "entry_uuid": "f90b0b81-5f26-11ed-a50d-919b4b1a5ec0", + "state": "Add", + "entry": { + "dn": "cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "attrs": { + "cn": [ + "testposix" + ], + "gidnumber": [ + "1234567" + ], + "ipauniqueid": [ + "fb64973e-5f26-11ed-9cfe-5254006b0418" + ], + "objectclass": [ + "groupofnames", + "ipaobject", + "ipausergroup", + "nestedgroup", + "posixgroup", + "top" + ] + } + } + } + ], + "delete_uuids": [], + "present_uuids": [] + } +} +"#; diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index c954922f9..4cbe29961 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -38,6 +38,7 @@ use webauthn_rs_proto::{ }; mod person; +mod scim; mod service_account; mod sync_account; mod system; diff --git a/kanidm_client/src/scim.rs b/kanidm_client/src/scim.rs new file mode 100644 index 000000000..c614e84e8 --- /dev/null +++ b/kanidm_client/src/scim.rs @@ -0,0 +1,16 @@ +use crate::{ClientError, KanidmClient}; +use kanidm_proto::scim_v1::{ScimSyncRequest, ScimSyncState}; + +impl KanidmClient { + pub async fn scim_v1_sync_status(&self) -> Result { + self.perform_get_request("/scim/v1/Sync").await + } + + pub async fn scim_v1_sync_update( + &self, + scim_sync_request: &ScimSyncRequest, + ) -> Result<(), ClientError> { + self.perform_post_request("/scim/v1/Sync", scim_sync_request) + .await + } +} diff --git a/kanidm_client/src/sync_account.rs b/kanidm_client/src/sync_account.rs index ccec69091..612fa1bc1 100644 --- a/kanidm_client/src/sync_account.rs +++ b/kanidm_client/src/sync_account.rs @@ -31,4 +31,21 @@ impl KanidmClient { self.perform_post_request("/v1/sync_account", new_acct) .await } + + pub async fn idm_sync_account_generate_token( + &self, + id: &str, + label: &str, + ) -> Result { + self.perform_post_request( + format!("/v1/sync_account/{}/_sync_token", id).as_str(), + label, + ) + .await + } + + pub async fn idm_sync_account_destroy_token(&self, id: &str) -> Result<(), ClientError> { + self.perform_delete_request(format!("/v1/sync_account/{}/_sync_token", id,).as_str()) + .await + } } diff --git a/kanidm_proto/Cargo.toml b/kanidm_proto/Cargo.toml index 000b4fd06..d5465a405 100644 --- a/kanidm_proto/Cargo.toml +++ b/kanidm_proto/Cargo.toml @@ -18,6 +18,7 @@ wasm = ["webauthn-rs-proto/wasm"] [dependencies] base32.workspace = true base64urlsafedata.workspace = true +scim_proto.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true time = { workspace = true, features = ["serde", "std"] } diff --git a/kanidm_proto/src/scim_v1.rs b/kanidm_proto/src/scim_v1.rs index 6e4a0eefb..3b350d061 100644 --- a/kanidm_proto/src/scim_v1.rs +++ b/kanidm_proto/src/scim_v1.rs @@ -1,9 +1,155 @@ use base64urlsafedata::Base64UrlSafeData; - use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use uuid::Uuid; + +pub use scim_proto::prelude::{ScimEntry, ScimError}; +use scim_proto::*; #[derive(Serialize, Deserialize, Debug)] pub enum ScimSyncState { - Initial, + Refresh, Active { cookie: Base64UrlSafeData }, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct ScimSyncRequest { + pub from_state: ScimSyncState, + pub to_state: ScimSyncState, + + // How do I want to represent different entities to kani? Split by type? All in one? + pub entries: Vec, + // Delete uuids? + pub delete_uuids: Vec, +} + +pub const SCIM_SCHEMA_SYNC_PERSON: &str = "urn:ietf:params:scim:schemas:kanidm:1.0:sync:person"; + +#[derive(Serialize, Debug, Clone)] +#[serde(into = "ScimEntry")] +pub struct ScimSyncPerson { + pub id: Uuid, + pub external_id: Option, + pub user_name: String, + pub display_name: String, + pub gidnumber: Option, + pub homedirectory: Option, + pub password_import: Option, + pub login_shell: Option, +} + +/* +impl TryFrom for ScimSyncPerson { + type Error = ScimError; + + fn try_from(_value: ScimEntry) -> Result { + todo!(); + } +} +*/ + +impl Into for ScimSyncPerson { + fn into(self) -> ScimEntry { + let ScimSyncPerson { + id, + external_id, + user_name, + display_name, + gidnumber, + homedirectory, + password_import, + login_shell, + } = self; + + let schemas = vec![SCIM_SCHEMA_SYNC_PERSON.to_string()]; + + let mut attrs = BTreeMap::default(); + + set_string!(attrs, "userName", user_name); + set_string!(attrs, "displayName", display_name); + set_option_u32!(attrs, "gidNumber", gidnumber); + set_option_string!(attrs, "homeDirectory", homedirectory); + set_option_string!(attrs, "passwordImport", password_import); + set_option_string!(attrs, "loginShell", login_shell); + + ScimEntry { + schemas, + id, + external_id, + meta: None, + attrs, + } + } +} + +pub const SCIM_SCHEMA_SYNC_GROUP: &str = "urn:ietf:params:scim:schemas:kanidm:1.0:sync:group"; + +#[derive(Serialize, Debug, Clone)] +pub struct ScimExternalMember { + pub external_id: String, +} + +impl Into for ScimExternalMember { + fn into(self) -> ScimComplexAttr { + let ScimExternalMember { external_id } = self; + let mut attrs = BTreeMap::default(); + + attrs.insert( + "external_id".to_string(), + ScimSimpleAttr::String(external_id), + ); + + ScimComplexAttr { attrs } + } +} + +#[derive(Serialize, Debug, Clone)] +#[serde(into = "ScimEntry")] +pub struct ScimSyncGroup { + pub id: Uuid, + pub external_id: Option, + pub name: String, + pub description: Option, + pub gidnumber: Option, + pub members: Vec, +} + +/* +impl TryFrom for ScimSyncPerson { + type Error = ScimError; + + fn try_from(_value: ScimEntry) -> Result { + todo!(); + } +} +*/ + +impl Into for ScimSyncGroup { + fn into(self) -> ScimEntry { + let ScimSyncGroup { + id, + external_id, + name, + description, + gidnumber, + members, + } = self; + + let schemas = vec![SCIM_SCHEMA_SYNC_GROUP.to_string()]; + + let mut attrs = BTreeMap::default(); + + set_string!(attrs, "name", name); + set_option_u32!(attrs, "gidNumber", gidnumber); + set_option_string!(attrs, "description", description); + set_multi_complex!(attrs, "members", members); + + ScimEntry { + schemas, + id, + external_id, + meta: None, + attrs, + } + } +} diff --git a/kanidm_tools/Cargo.toml b/kanidm_tools/Cargo.toml index 0b1378b86..578b94ca9 100644 --- a/kanidm_tools/Cargo.toml +++ b/kanidm_tools/Cargo.toml @@ -2,7 +2,7 @@ name = "kanidm_tools" default-run = "kanidm" description = "Kanidm Client Tools" -documentation = "https://docs.rs/kanidm_tools/latest/kanidm_tools/" +documentation = "https://kanidm.github.io/kanidm/stable/" version.workspace = true authors.workspace = true diff --git a/kanidm_tools/src/cli/lib.rs b/kanidm_tools/src/cli/lib.rs index bddf9ec46..bc927fa0c 100644 --- a/kanidm_tools/src/cli/lib.rs +++ b/kanidm_tools/src/cli/lib.rs @@ -30,6 +30,7 @@ pub mod raw; pub mod recycle; pub mod serviceaccount; pub mod session; +pub mod synch; impl SelfOpt { pub fn debug(&self) -> bool { @@ -68,6 +69,7 @@ impl SystemOpt { SystemOpt::PwBadlist { commands } => commands.debug(), SystemOpt::Oauth2 { commands } => commands.debug(), SystemOpt::Domain { commands } => commands.debug(), + SystemOpt::Synch { commands } => commands.debug(), } } @@ -76,6 +78,7 @@ impl SystemOpt { SystemOpt::PwBadlist { commands } => commands.exec().await, SystemOpt::Oauth2 { commands } => commands.exec().await, SystemOpt::Domain { commands } => commands.exec().await, + SystemOpt::Synch { commands } => commands.exec().await, } } } diff --git a/kanidm_tools/src/cli/synch.rs b/kanidm_tools/src/cli/synch.rs new file mode 100644 index 000000000..5dc055a7d --- /dev/null +++ b/kanidm_tools/src/cli/synch.rs @@ -0,0 +1,68 @@ +use crate::SynchOpt; + +impl SynchOpt { + pub fn debug(&self) -> bool { + match self { + SynchOpt::List(copt) => copt.debug, + SynchOpt::Get(nopt) => nopt.copt.debug, + SynchOpt::Create { copt, .. } + | SynchOpt::GenerateToken { copt, .. } + | SynchOpt::DestroyToken { copt, .. } => copt.debug, + } + } + + pub async fn exec(&self) { + match self { + SynchOpt::List(copt) => { + let client = copt.to_client().await; + match client.idm_sync_account_list().await { + Ok(r) => r.iter().for_each(|ent| println!("{}", ent)), + Err(e) => error!("Error -> {:?}", e), + } + } + SynchOpt::Get(nopt) => { + let client = nopt.copt.to_client().await; + match client.idm_sync_account_get(nopt.name.as_str()).await { + Ok(Some(e)) => println!("{}", e), + Ok(None) => println!("No matching entries"), + Err(e) => error!("Error -> {:?}", e), + } + } + SynchOpt::Create { + account_id, + copt, + description, + } => { + let client = copt.to_client().await; + match client + .idm_sync_account_create(&account_id, description.as_deref()) + .await + { + Ok(()) => println!("Success"), + Err(e) => error!("Error -> {:?}", e), + } + } + SynchOpt::GenerateToken { + account_id, + label, + copt, + } => { + let client = copt.to_client().await; + match client + .idm_sync_account_generate_token(&account_id, &label) + .await + { + Ok(token) => println!("token: {}", token), + Err(e) => error!("Error -> {:?}", e), + } + } + SynchOpt::DestroyToken { account_id, copt } => { + let client = copt.to_client().await; + match client.idm_sync_account_destroy_token(&account_id).await { + Ok(()) => println!("Success"), + Err(e) => error!("Error -> {:?}", e), + } + } + } + } +} diff --git a/kanidm_tools/src/opt/kanidm.rs b/kanidm_tools/src/opt/kanidm.rs index 03382b22d..9cdfff4a4 100644 --- a/kanidm_tools/src/opt/kanidm.rs +++ b/kanidm_tools/src/opt/kanidm.rs @@ -732,6 +732,42 @@ pub enum DomainOpt { ResetTokenKey(CommonOpt), } +#[derive(Debug, Subcommand)] +pub enum SynchOpt { + #[clap(name = "list")] + /// List all configured IDM sync accounts + List(CommonOpt), + #[clap(name = "get")] + /// Display a selected IDM sync account + Get(Named), + /// Create a new IDM sync account + #[clap(name = "create")] + Create { + #[clap()] + account_id: String, + #[clap(flatten)] + copt: CommonOpt, + #[clap(name = "description")] + description: Option, + }, + #[clap(name = "generate-token")] + GenerateToken { + #[clap()] + account_id: String, + #[clap()] + label: String, + #[clap(flatten)] + copt: CommonOpt, + }, + #[clap(name = "destroy-token")] + DestroyToken { + #[clap()] + account_id: String, + #[clap(flatten)] + copt: CommonOpt, + }, +} + #[derive(Debug, Subcommand)] pub enum SystemOpt { #[clap(name = "pw-badlist")] @@ -752,6 +788,11 @@ pub enum SystemOpt { #[clap(subcommand)] commands: DomainOpt, }, + #[clap(name = "sync", hide = true)] + Synch { + #[clap(subcommand)] + commands: SynchOpt, + } } #[derive(Debug, Subcommand)] diff --git a/kanidmd/core/src/actors/v1_read.rs b/kanidmd/core/src/actors/v1_read.rs index d8b72d6cd..da4c91bbe 100644 --- a/kanidmd/core/src/actors/v1_read.rs +++ b/kanidmd/core/src/actors/v1_read.rs @@ -37,7 +37,7 @@ use kanidmd_lib::{ // =========================================================== pub struct QueryServerReadV1 { - idms: Arc, + pub(crate) idms: Arc, ldap: Arc, } diff --git a/kanidmd/core/src/actors/v1_scim.rs b/kanidmd/core/src/actors/v1_scim.rs index 4c140ba55..f7c67eda0 100644 --- a/kanidmd/core/src/actors/v1_scim.rs +++ b/kanidmd/core/src/actors/v1_scim.rs @@ -1,9 +1,11 @@ use kanidmd_lib::prelude::*; -use crate::QueryServerWriteV1; -use kanidmd_lib::idm::scim::GenerateScimSyncTokenEvent; +use crate::{QueryServerReadV1, QueryServerWriteV1}; +use kanidmd_lib::idm::scim::{GenerateScimSyncTokenEvent, ScimSyncUpdateEvent}; use kanidmd_lib::idm::server::IdmServerTransaction; +use kanidm_proto::scim_v1::{ScimSyncRequest, ScimSyncState}; + impl QueryServerWriteV1 { #[instrument( level = "info", @@ -77,4 +79,48 @@ impl QueryServerWriteV1 { .sync_account_destroy_token(&ident, target, ct) .and_then(|r| idms_prox_write.commit().map(|_| r)) } + + #[instrument( + level = "info", + skip_all, + fields(uuid = ?eventid) + )] + pub async fn handle_scim_sync_apply( + &self, + bearer: Option, + changes: ScimSyncRequest, + eventid: Uuid, + ) -> Result<(), OperationError> { + let ct = duration_from_epoch_now(); + let mut idms_prox_write = self.idms.proxy_write(ct).await; + + let ident = + idms_prox_write.validate_and_parse_sync_token_to_ident(bearer.as_deref(), ct)?; + + let sse = ScimSyncUpdateEvent { ident }; + + idms_prox_write + .scim_sync_apply(&sse, &changes, ct) + .and_then(|r| idms_prox_write.commit().map(|_| r)) + } +} + +impl QueryServerReadV1 { + #[instrument( + level = "info", + skip_all, + fields(uuid = ?eventid) + )] + pub async fn handle_scim_sync_status( + &self, + bearer: Option, + eventid: Uuid, + ) -> Result { + let ct = duration_from_epoch_now(); + let idms_prox_read = self.idms.proxy_read().await; + + let ident = idms_prox_read.validate_and_parse_sync_token_to_ident(bearer.as_deref(), ct)?; + + idms_prox_read.scim_sync_get_state(&ident) + } } diff --git a/kanidmd/core/src/https/mod.rs b/kanidmd/core/src/https/mod.rs index 9a19f415d..f25c636f9 100644 --- a/kanidmd/core/src/https/mod.rs +++ b/kanidmd/core/src/https/mod.rs @@ -72,7 +72,7 @@ pub struct AppState { pub trait RequestExtensions { fn get_current_uat(&self) -> Option; - fn get_auth_bearer(&self) -> Option<&str>; + fn get_auth_bearer(&self) -> Option; fn get_current_auth_session_id(&self) -> Option; @@ -84,7 +84,7 @@ pub trait RequestExtensions { } impl RequestExtensions for tide::Request { - fn get_auth_bearer(&self) -> Option<&str> { + fn get_auth_bearer(&self) -> Option { // Contact the QS to get it to validate wtf is up. // let kref = &self.state().bundy_handle; // self.session().get::("uat") @@ -97,6 +97,7 @@ impl RequestExtensions for tide::Request { // Turn it to a &str, and then check the prefix h.as_str().strip_prefix("Bearer ") }) + .map(str::to_string) } fn get_current_uat(&self) -> Option { diff --git a/kanidmd/core/src/https/v1_scim.rs b/kanidmd/core/src/https/v1_scim.rs index 243003f62..57a8713c6 100644 --- a/kanidmd/core/src/https/v1_scim.rs +++ b/kanidmd/core/src/https/v1_scim.rs @@ -1,5 +1,6 @@ use super::routemaps::{RouteMap, RouteMaps}; use super::{to_tide_response, AppState, RequestExtensions}; +use kanidm_proto::scim_v1::ScimSyncRequest; use kanidm_proto::v1::Entry as ProtoEntry; use kanidmd_lib::prelude::*; @@ -93,32 +94,36 @@ pub async fn sync_account_token_delete(req: tide::Request) -> tide::Re to_tide_response(res, hvalue) } -async fn scim_sync_post(_req: tide::Request) -> tide::Result { - // let (eventid, hvalue) = req.new_eventid(); - /* - let ApiTokenGenerate { - label, - expiry, - read_write, - } = req.body_json().await?; - */ +async fn scim_sync_post(mut req: tide::Request) -> tide::Result { + let (eventid, hvalue) = req.new_eventid(); - // We need to deserialise the body. + // Given the token, and a sync update, apply the changes if any + let bearer = req.get_auth_bearer(); - Ok(tide::Response::new(500)) + // Change this type later. + let changes: ScimSyncRequest = req.body_json().await?; + + let res = req + .state() + .qe_w_ref + .handle_scim_sync_apply(bearer, changes, eventid) + .await; + to_tide_response(res, hvalue) } -async fn scim_sync_get(_req: tide::Request) -> tide::Result { - // let (eventid, hvalue) = req.new_eventid(); +async fn scim_sync_get(req: tide::Request) -> tide::Result { + let (eventid, hvalue) = req.new_eventid(); - // let bearer = req.get_auth_bearer(); + // Given the token, what is it's connected sync state? + let bearer = req.get_auth_bearer(); + trace!(?bearer); - // Given the token - // What is the connected sync session - // Issue it's current state (version) cookie. - - // todo!(); - Ok(tide::Response::new(500)) + let res = req + .state() + .qe_r_ref + .handle_scim_sync_status(bearer, eventid) + .await; + to_tide_response(res, hvalue) } async fn scim_sink_get(req: tide::Request) -> tide::Result { diff --git a/kanidmd/lib/src/constants/acp.rs b/kanidmd/lib/src/constants/acp.rs index 0935d85e4..2f370da2b 100644 --- a/kanidmd/lib/src/constants/acp.rs +++ b/kanidmd/lib/src/constants/acp.rs @@ -1327,7 +1327,8 @@ pub const JSON_IDM_HP_ACP_SYNC_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{ ], "acp_modify_presentattr": [ "name", - "description" + "description", + "sync_token_session" ], "acp_modify_class": [], "acp_create_attr": [ diff --git a/kanidmd/lib/src/entry.rs b/kanidmd/lib/src/entry.rs index 626d5ee85..39718a28a 100644 --- a/kanidmd/lib/src/entry.rs +++ b/kanidmd/lib/src/entry.rs @@ -1709,7 +1709,7 @@ impl Entry { // so they are all in "ldap forms" which makes our next stage a bit easier. // Stage 1 - transform our results to a map of kani attr -> ldap value. - let attr_map: Result>, _> = self + let attr_map: Result>>, _> = self .attrs .iter() .map(|(k, vs)| { @@ -1717,8 +1717,8 @@ impl Entry { .map(|pvs| (k.as_str(), pvs)) }) .collect(); - let attr_map = attr_map?; + // Stage 2 - transform and get all our attr - names out that we need to return. // ldap a, kani a let attr_names: Vec<(&str, &str)> = if all_attrs { @@ -1748,7 +1748,7 @@ impl Entry { match ldap_a { "entrydn" => Some(LdapPartialAttribute { atype: "entrydn".to_string(), - vals: vec![dn.clone()], + vals: vec![dn.as_bytes().to_vec()], }), _ => attr_map.get(kani_a).map(|pvs| LdapPartialAttribute { atype: ldap_a.to_string(), diff --git a/kanidmd/lib/src/idm/scim.rs b/kanidmd/lib/src/idm/scim.rs index efb2e13e4..da60179cf 100644 --- a/kanidmd/lib/src/idm/scim.rs +++ b/kanidmd/lib/src/idm/scim.rs @@ -4,6 +4,7 @@ use std::time::Duration; use base64urlsafedata::Base64UrlSafeData; use compact_jwt::{Jws, JwsSigner}; +use kanidm_proto::scim_v1::ScimSyncRequest; use kanidm_proto::scim_v1::*; use kanidm_proto::v1::ApiTokenPurpose; use serde::{Deserialize, Serialize}; @@ -225,14 +226,38 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { } } -pub struct ScimSyncUpdateEvent {} +pub struct ScimSyncUpdateEvent { + pub ident: Identity, +} impl<'a> IdmServerProxyWriteTransaction<'a> { - pub fn scim_sync_update( + pub fn scim_sync_apply( &mut self, - _sse: &ScimSyncUpdateEvent, + sse: &ScimSyncUpdateEvent, + _changes: &ScimSyncRequest, _ct: Duration, ) -> Result<(), OperationError> { + let _sync_uuid = match &sse.ident.origin { + IdentType::User(_) | IdentType::Internal => { + warn!("Ident type is not synchronise"); + return Err(OperationError::AccessDenied); + } + IdentType::Synch(u) => { + // Ok! + u + } + }; + + match sse.ident.access_scope() { + AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::ReadWrite => { + warn!("Ident access scope is not synchronise"); + return Err(OperationError::AccessDenied); + } + AccessScope::Synchronise => { + // As you were + } + }; + // Only update entries related to this uuid // Make a sync_authority uuid to relate back to on creates. @@ -283,7 +308,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> { Some(b) => ScimSyncState::Active { cookie: Base64UrlSafeData(b.to_vec()), }, - None => ScimSyncState::Initial, + None => ScimSyncState::Refresh, }, ) } @@ -300,7 +325,7 @@ mod tests { use kanidm_proto::v1::ApiTokenPurpose; use std::time::Duration; - use super::{GenerateScimSyncTokenEvent, ScimSyncToken}; + use super::{GenerateScimSyncTokenEvent, ScimSyncToken, ScimSyncUpdateEvent}; use async_std::task; @@ -359,7 +384,7 @@ mod tests { .expect("Failed to get current sync state"); trace!(?sync_state); - assert!(matches!(sync_state, ScimSyncState::Initial)); + assert!(matches!(sync_state, ScimSyncState::Refresh)); drop(idms_prox_read); @@ -497,4 +522,141 @@ mod tests { } // Need to delete different phases such as conflictn and end of the agreement. + + #[test] + fn test_idm_scim_sync_refresh_1() { + run_idm_test!(|_qs: &QueryServer, + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed| { + let ct = Duration::from_secs(TEST_CURRENT_TIME); + + let mut idms_prox_write = task::block_on(idms.proxy_write(ct)); + + let sync_uuid = Uuid::new_v4(); + + let e1 = entry_init!( + ("class", Value::new_class("object")), + ("class", Value::new_class("sync_account")), + ("name", Value::new_iname("test_scim_sync")), + ("uuid", Value::new_uuid(sync_uuid)), + ("description", Value::new_utf8s("A test sync agreement")) + ); + + let ce = CreateEvent::new_internal(vec![e1]); + let cr = idms_prox_write.qs_write.create(&ce); + assert!(cr.is_ok()); + + let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector"); + + let sync_token = idms_prox_write + .scim_sync_generate_token(>e, ct) + .expect("failed to generate new scim sync token"); + + let ident = idms_prox_write + .validate_and_parse_sync_token_to_ident(Some(sync_token.as_str()), ct) + .expect("Failed to process sync token to ident"); + + let sse = ScimSyncUpdateEvent { ident }; + + let changes = + serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync"); + + let res = idms_prox_write.scim_sync_apply(&sse, &changes, ct); + + // Currently in testing this is just access denied. + assert!(matches!(res, Err(OperationError::AccessDenied))); + + assert!(idms_prox_write.commit().is_ok()); + }) + } + + const TEST_SYNC_SCIM_IPA_1: &str = r#" + { + "from_state": "Refresh", + "to_state": { + "Active": { + "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSghKGNuPWFkbWlucykpKCEoY249aXBhdXNlcnMpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzEwOQ" + } + }, + "entries": [ + { + "schemas": [ + "urn:ietf:params:scim:schemas:kanidm:1.0:sync:person" + ], + "id": "ac60034b-3498-11ed-a50d-919b4b1a5ec0", + "externalId": "uid=admin,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "displayName": "Administrator", + "gidNumber": 8200000, + "homeDirectory": "/home/admin", + "loginShell": "/bin/bash", + "passwordImport": "CVBguEizG80swI8sftaknw", + "userName": "admin" + }, + { + "schemas": [ + "urn:ietf:params:scim:schemas:kanidm:1.0:sync:group" + ], + "id": "ac60034e-3498-11ed-a50d-919b4b1a5ec0", + "externalId": "cn=editors,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "description": "Limited admins who can edit other users", + "gidNumber": 8200002, + "name": "editors" + }, + { + "schemas": [ + "urn:ietf:params:scim:schemas:kanidm:1.0:sync:group" + ], + "id": "0c56a965-3499-11ed-a50d-919b4b1a5ec0", + "externalId": "cn=trust admins,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "description": "Trusts administrators group", + "members": [ + { + "external_id": "uid=admin,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au" + } + ], + "name": "trust admins" + }, + { + "schemas": [ + "urn:ietf:params:scim:schemas:kanidm:1.0:sync:person" + ], + "id": "babb8302-43a1-11ed-a50d-919b4b1a5ec0", + "externalId": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "displayName": "Test User", + "gidNumber": 12345, + "homeDirectory": "/home/testuser", + "loginShell": "/bin/sh", + "passwordImport": "iEb36u6PsRetBr3YMLdYbA", + "userName": "testuser" + }, + { + "schemas": [ + "urn:ietf:params:scim:schemas:kanidm:1.0:sync:group" + ], + "id": "d547c581-5f26-11ed-a50d-919b4b1a5ec0", + "externalId": "cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "description": "Test group", + "name": "testgroup" + }, + { + "schemas": [ + "urn:ietf:params:scim:schemas:kanidm:1.0:sync:group" + ], + "id": "d547c583-5f26-11ed-a50d-919b4b1a5ec0", + "externalId": "cn=testexternal,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "name": "testexternal" + }, + { + "schemas": [ + "urn:ietf:params:scim:schemas:kanidm:1.0:sync:group" + ], + "id": "f90b0b81-5f26-11ed-a50d-919b4b1a5ec0", + "externalId": "cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au", + "gidNumber": 1234567, + "name": "testposix" + } + ], + "delete_uuids": [] + } + "#; } diff --git a/kanidmd/lib/src/ldap.rs b/kanidmd/lib/src/ldap.rs index 3a08c698c..a183dfb68 100644 --- a/kanidmd/lib/src/ldap.rs +++ b/kanidmd/lib/src/ldap.rs @@ -86,31 +86,31 @@ impl LdapServer { attributes: vec![ LdapPartialAttribute { atype: "objectClass".to_string(), - vals: vec!["top".to_string()], + vals: vec!["top".as_bytes().to_vec()], }, LdapPartialAttribute { atype: "vendorName".to_string(), - vals: vec!["Kanidm Project".to_string()], + vals: vec!["Kanidm Project".as_bytes().to_vec()], }, LdapPartialAttribute { atype: "vendorVersion".to_string(), - vals: vec!["kanidm_ldap_1.0.0".to_string()], + vals: vec!["kanidm_ldap_1.0.0".as_bytes().to_vec()], }, LdapPartialAttribute { atype: "supportedLDAPVersion".to_string(), - vals: vec!["3".to_string()], + vals: vec!["3".as_bytes().to_vec()], }, LdapPartialAttribute { atype: "supportedExtension".to_string(), - vals: vec!["1.3.6.1.4.1.4203.1.11.3".to_string()], + vals: vec!["1.3.6.1.4.1.4203.1.11.3".as_bytes().to_vec()], }, LdapPartialAttribute { atype: "supportedFeatures".to_string(), - vals: vec!["1.3.6.1.4.1.4203.1.5.1".to_string()], + vals: vec!["1.3.6.1.4.1.4203.1.5.1".as_bytes().to_vec()], }, LdapPartialAttribute { atype: "defaultnamingcontext".to_string(), - vals: vec![basedn.clone()], + vals: vec![basedn.as_bytes().to_vec()], }, ], }; @@ -717,12 +717,12 @@ mod tests { let mut attrs = HashSet::new(); for a in $e.attributes.iter() { for v in a.vals.iter() { - attrs.insert((a.atype.as_str(), v.as_str())); + attrs.insert((a.atype.as_str(), v.as_slice())); } }; $( assert!(attrs.contains(&( - $item.0, $item.1 + $item.0, $item.1.as_bytes() ))); )* diff --git a/kanidmd/lib/src/server.rs b/kanidmd/lib/src/server.rs index b3d3ef57e..68b22c9a9 100644 --- a/kanidmd/lib/src/server.rs +++ b/kanidmd/lib/src/server.rs @@ -688,22 +688,25 @@ pub trait QueryServerTransaction<'a> { &self, value: &ValueSet, basedn: &str, - ) -> Result, OperationError> { + ) -> Result>, OperationError> { if let Some(r_set) = value.as_refer_set() { let v: Result, _> = r_set .iter() .copied() .map(|ur| { let rdn = self.uuid_to_rdn(ur)?; - Ok(format!("{},{}", rdn, basedn)) + Ok(format!("{},{}", rdn, basedn).into_bytes()) }) .collect(); v } else if let Some(k_set) = value.as_sshkey_map() { - let v: Vec<_> = k_set.values().cloned().collect(); + let v: Vec<_> = k_set.values().cloned().map(|s| s.into_bytes()).collect(); Ok(v) } else { - let v: Vec<_> = value.to_proto_string_clone_iter().collect(); + let v: Vec<_> = value + .to_proto_string_clone_iter() + .map(|s| s.into_bytes()) + .collect(); Ok(v) } } diff --git a/orca/src/ds.rs b/orca/src/ds.rs index 5239ef54b..8de195b43 100644 --- a/orca/src/ds.rs +++ b/orca/src/ds.rs @@ -97,11 +97,14 @@ impl DirectoryServer { attributes: vec![ LdapAttribute { atype: "objectClass".to_string(), - vals: vec!["top".to_string(), "organizationalUnit".to_string()], + vals: vec![ + "top".as_bytes().into(), + "organizationalUnit".as_bytes().into(), + ], }, LdapAttribute { atype: "ou".to_string(), - vals: vec!["people".to_string()], + vals: vec!["people".as_bytes().into()], }, ], }; @@ -121,11 +124,14 @@ impl DirectoryServer { attributes: vec![ LdapAttribute { atype: "objectClass".to_string(), - vals: vec!["top".to_string(), "organizationalUnit".to_string()], + vals: vec![ + "top".as_bytes().into(), + "organizationalUnit".as_bytes().into(), + ], }, LdapAttribute { atype: "ou".to_string(), - vals: vec!["groups".to_string()], + vals: vec!["groups".as_bytes().into()], }, ], }; @@ -155,40 +161,40 @@ impl DirectoryServer { LdapAttribute { atype: "objectClass".to_string(), vals: vec![ - "top".to_string(), - "nsPerson".to_string(), - "nsAccount".to_string(), - "nsOrgPerson".to_string(), - "posixAccount".to_string(), + "top".as_bytes().into(), + "nsPerson".as_bytes().into(), + "nsAccount".as_bytes().into(), + "nsOrgPerson".as_bytes().into(), + "posixAccount".as_bytes().into(), ], }, LdapAttribute { atype: "cn".to_string(), - vals: vec![a.uuid.to_string()], + vals: vec![a.uuid.as_bytes().to_vec()], }, LdapAttribute { atype: "uid".to_string(), - vals: vec![a.name.clone()], + vals: vec![a.name.as_bytes().into()], }, LdapAttribute { atype: "displayName".to_string(), - vals: vec![a.display_name.clone()], + vals: vec![a.display_name.as_bytes().into()], }, LdapAttribute { atype: "userPassword".to_string(), - vals: vec![a.password.clone()], + vals: vec![a.password.as_bytes().into()], }, LdapAttribute { atype: "homeDirectory".to_string(), - vals: vec![format!("/home/{}", a.uuid)], + vals: vec![format!("/home/{}", a.uuid).as_bytes().into()], }, LdapAttribute { atype: "uidNumber".to_string(), - vals: vec!["1000".to_string()], + vals: vec!["1000".as_bytes().into()], }, LdapAttribute { atype: "gidNumber".to_string(), - vals: vec!["1000".to_string()], + vals: vec!["1000".as_bytes().into()], }, ], }; @@ -200,11 +206,14 @@ impl DirectoryServer { attributes: vec![ LdapAttribute { atype: "objectClass".to_string(), - vals: vec!["top".to_string(), "groupOfNames".to_string()], + vals: vec![ + "top".as_bytes().into(), + "groupOfNames".as_bytes().into(), + ], }, LdapAttribute { atype: "cn".to_string(), - vals: vec![g.uuid.to_string(), g.name.clone()], + vals: vec![g.uuid.as_bytes().to_vec(), g.name.as_bytes().into()], }, ], }; @@ -222,7 +231,7 @@ impl DirectoryServer { } }) { // List of dns - let vals: Vec<_> = g + let vals: Vec> = g .members .iter() .map(|id| { @@ -230,6 +239,8 @@ impl DirectoryServer { .get(id) .unwrap() .get_ds_ldap_dn(&self.ldap.basedn) + .as_bytes() + .into() }) .collect(); @@ -270,11 +281,11 @@ impl DirectoryServer { attributes: vec![ LdapAttribute { atype: "objectClass".to_string(), - vals: vec!["top".to_string(), "groupOfNames".to_string()], + vals: vec!["top".as_bytes().into(), "groupOfNames".as_bytes().into()], }, LdapAttribute { atype: "cn".to_string(), - vals: vec!["priv_account_manage".to_string()], + vals: vec!["priv_account_manage".as_bytes().into()], }, ], }; @@ -297,11 +308,11 @@ impl DirectoryServer { attributes: vec![ LdapAttribute { atype: "objectClass".to_string(), - vals: vec!["top".to_string(), "groupOfNames".to_string()], + vals: vec!["top".as_bytes().into(), "groupOfNames".as_bytes().into()], }, LdapAttribute { atype: "cn".to_string(), - vals: vec!["priv_group_manage".to_string()], + vals: vec!["priv_group_manage".as_bytes().into()], }, ], }; @@ -317,14 +328,14 @@ impl DirectoryServer { modification: LdapPartialAttribute { atype: "aci".to_string(), vals: vec![ - r#"(targetattr="dc || description || objectClass")(targetfilter="(objectClass=domain)")(version 3.0; acl "Enable anyone domain read"; allow (read, search, compare)(userdn="ldap:///anyone");)"#.to_string(), + r#"(targetattr="dc || description || objectClass")(targetfilter="(objectClass=domain)")(version 3.0; acl "Enable anyone domain read"; allow (read, search, compare)(userdn="ldap:///anyone");)"#.as_bytes().into(), - r#"(targetattr="ou || objectClass")(targetfilter="(objectClass=organizationalUnit)")(version 3.0; acl "Enable anyone ou read"; allow (read, search, compare)(userdn="ldap:///anyone");)"#.to_string(), - r#"(targetattr="cn || member || gidNumber || nsUniqueId || description || objectClass")(targetfilter="(objectClass=groupOfNames)")(version 3.0; acl "Enable anyone group read"; allow (read, search, compare)(userdn="ldap:///anyone");)"#.to_string(), - format!(r#"(targetattr="cn || member || gidNumber || description || objectClass")(targetfilter="(objectClass=groupOfNames)")(version 3.0; acl "Enable group_admin to manage groups"; allow (write,add, delete)(groupdn="ldap:///cn=priv_group_manage,{}");)"#, self.ldap.basedn), - r#"(targetattr="objectClass || description || nsUniqueId || uid || displayName || loginShell || uidNumber || gidNumber || gecos || homeDirectory || cn || memberOf || mail || nsSshPublicKey || nsAccountLock || userCertificate")(targetfilter="(objectClass=posixaccount)")(version 3.0; acl "Enable anyone user read"; allow (read, search, compare)(userdn="ldap:///anyone");)"#.to_string(), - r#"(targetattr="displayName || legalName || userPassword || nsSshPublicKey")(version 3.0; acl "Enable self partial modify"; allow (write)(userdn="ldap:///self");)"#.to_string(), - format!(r#"(targetattr="uid || description || displayName || loginShell || uidNumber || gidNumber || gecos || homeDirectory || cn || memberOf || mail || legalName || telephoneNumber || mobile")(targetfilter="(&(objectClass=nsPerson)(objectClass=nsAccount))")(version 3.0; acl "Enable user admin create"; allow (write, add, delete, read)(groupdn="ldap:///cn=priv_account_manage,{}");)"#, self.ldap.basedn), + r#"(targetattr="ou || objectClass")(targetfilter="(objectClass=organizationalUnit)")(version 3.0; acl "Enable anyone ou read"; allow (read, search, compare)(userdn="ldap:///anyone");)"#.as_bytes().into(), + r#"(targetattr="cn || member || gidNumber || nsUniqueId || description || objectClass")(targetfilter="(objectClass=groupOfNames)")(version 3.0; acl "Enable anyone group read"; allow (read, search, compare)(userdn="ldap:///anyone");)"#.as_bytes().into(), + format!(r#"(targetattr="cn || member || gidNumber || description || objectClass")(targetfilter="(objectClass=groupOfNames)")(version 3.0; acl "Enable group_admin to manage groups"; allow (write,add, delete)(groupdn="ldap:///cn=priv_group_manage,{}");)"#, self.ldap.basedn).as_bytes().into(), + r#"(targetattr="objectClass || description || nsUniqueId || uid || displayName || loginShell || uidNumber || gidNumber || gecos || homeDirectory || cn || memberOf || mail || nsSshPublicKey || nsAccountLock || userCertificate")(targetfilter="(objectClass=posixaccount)")(version 3.0; acl "Enable anyone user read"; allow (read, search, compare)(userdn="ldap:///anyone");)"#.as_bytes().into(), + r#"(targetattr="displayName || legalName || userPassword || nsSshPublicKey")(version 3.0; acl "Enable self partial modify"; allow (write)(userdn="ldap:///self");)"#.as_bytes().into(), + format!(r#"(targetattr="uid || description || displayName || loginShell || uidNumber || gidNumber || gecos || homeDirectory || cn || memberOf || mail || legalName || telephoneNumber || mobile")(targetfilter="(&(objectClass=nsPerson)(objectClass=nsAccount))")(version 3.0; acl "Enable user admin create"; allow (write, add, delete, read)(groupdn="ldap:///cn=priv_account_manage,{}");)"#, self.ldap.basedn).as_bytes().into(), ] } } @@ -352,10 +363,20 @@ impl DirectoryServer { == 0; if need_account { - priv_account.push(account.get_ds_ldap_dn(&self.ldap.basedn)) + priv_account.push( + account + .get_ds_ldap_dn(&self.ldap.basedn) + .as_bytes() + .to_vec(), + ) } if need_group { - priv_group.push(account.get_ds_ldap_dn(&self.ldap.basedn)) + priv_group.push( + account + .get_ds_ldap_dn(&self.ldap.basedn) + .as_bytes() + .to_vec(), + ) } }