20221103 ipa import driver (#1180)

This commit is contained in:
Firstyear 2022-11-10 07:43:22 +10:00 committed by GitHub
parent 2e864be37f
commit 1ed4d7c1bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1721 additions and 123 deletions

122
Cargo.lock generated
View file

@ -134,7 +134,7 @@ dependencies = [
"num-traits", "num-traits",
"rusticata-macros", "rusticata-macros",
"thiserror", "thiserror",
"time 0.3.16", "time 0.3.17",
] ]
[[package]] [[package]]
@ -596,9 +596,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.12.2" version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aec14f5d4e6e3f927cd0c81f72e5710d95ee9019fbeb4b3021193867491bfd8" checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@ -769,9 +769,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "compact_jwt" name = "compact_jwt"
version = "0.2.8" version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5656b98b1584764a52906e67caec20dfb9b0179ac2052d0d5937b083bc39a120" checksum = "51f9032b96a89dd79ffc5f62523d5351ebb40680cbdfc4029393b511b9e971aa"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"base64urlsafedata", "base64urlsafedata",
@ -869,7 +869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917"
dependencies = [ dependencies = [
"percent-encoding", "percent-encoding",
"time 0.3.16", "time 0.3.17",
"version_check", "version_check",
] ]
@ -885,7 +885,7 @@ dependencies = [
"publicsuffix", "publicsuffix",
"serde", "serde",
"serde_json", "serde_json",
"time 0.3.16", "time 0.3.17",
"url", "url",
] ]
@ -2193,9 +2193,9 @@ dependencies = [
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.5.0" version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745"
[[package]] [[package]]
name = "itertools" name = "itertools"
@ -2245,6 +2245,27 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "kanidm_client" name = "kanidm_client"
version = "1.1.0-alpha.11-dev" version = "1.1.0-alpha.11-dev"
@ -2269,6 +2290,7 @@ dependencies = [
"base32", "base32",
"base64urlsafedata", "base64urlsafedata",
"last-git-commit", "last-git-commit",
"scim_proto",
"serde", "serde",
"serde_json", "serde_json",
"time 0.2.27", "time 0.2.27",
@ -2518,16 +2540,36 @@ dependencies = [
"nom 2.2.1", "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]] [[package]]
name = "ldap3_proto" name = "ldap3_proto"
version = "0.2.3" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/kanidm/ldap3.git#6b0d146d3f85a32add3bdc3639ba4146822eb861"
checksum = "62d7f04b6dc4d5401b817596e424ecb4a0931db1418f3987a27e0ab69320665e"
dependencies = [ dependencies = [
"bytes", "bytes",
"lber", "lber",
"nom 7.1.1",
"tokio-util", "tokio-util",
"tracing", "tracing",
"uuid",
] ]
[[package]] [[package]]
@ -2768,9 +2810,9 @@ dependencies = [
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.10" version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
@ -2886,9 +2928,9 @@ dependencies = [
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.13.1" version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
@ -2915,15 +2957,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "oauth2" name = "oauth2"
version = "4.2.3" version = "4.2.3"
@ -3256,9 +3289,9 @@ dependencies = [
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
@ -3539,9 +3572,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.27" version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
@ -3747,6 +3780,21 @@ dependencies = [
"parking_lot", "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]] [[package]]
name = "scoped-tls" name = "scoped-tls"
version = "1.0.1" version = "1.0.1"
@ -4411,16 +4459,14 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.16" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [ dependencies = [
"itoa 1.0.4", "itoa 1.0.4",
"libc",
"num_threads",
"serde", "serde",
"time-core", "time-core",
"time-macros 0.2.5", "time-macros 0.2.6",
] ]
[[package]] [[package]]
@ -4441,9 +4487,9 @@ dependencies = [
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
dependencies = [ dependencies = [
"time-core", "time-core",
] ]
@ -5250,7 +5296,7 @@ dependencies = [
"oid-registry", "oid-registry",
"rusticata-macros", "rusticata-macros",
"thiserror", "thiserror",
"time 0.3.16", "time 0.3.17",
] ]
[[package]] [[package]]
@ -5370,5 +5416,5 @@ dependencies = [
"lazy_static", "lazy_static",
"quick-error", "quick-error",
"regex", "regex",
"time 0.3.16", "time 0.3.17",
] ]

View file

@ -5,6 +5,7 @@ lto = "thin"
[workspace] [workspace]
members = [ members = [
"iam_migrations/freeipa",
"kanidm_client", "kanidm_client",
"kanidm_proto", "kanidm_proto",
"kanidm_tools", "kanidm_tools",
@ -83,7 +84,17 @@ kanidm_unix_int = { path = "./kanidm_unix_int" }
last-git-commit = "0.2.0" last-git-commit = "0.2.0"
# REMOVE this # REMOVE this
lazy_static = "^1.4.0" 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" libc = "^0.2.135"
libnss = "^0.4.0" libnss = "^0.4.0"
libsqlite3-sys = "^0.25.0" libsqlite3-sys = "^0.25.0"

View file

@ -1,4 +1,4 @@
dn: cn=Retro Changelog Plugin,cn=plugins,cn=config dn: cn=Retro Changelog Plugin,cn=plugins,cn=config
changetype: modify changetype: modify
add: nsslapd-include-suffix add: nsslapd-include-suffix
nsslapd-include-suffix: cn=accounts,dc=dev,dc=kanidm,dc=com nsslapd-include-suffix: dc=dev,dc=kanidm,dc=com

View file

@ -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

View file

@ -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,
}

View file

@ -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<ScimSyncRequest, ()> {
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::<Result<Vec<_>, _>>();
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<Option<ScimEntry>, ()> {
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!");
}

View file

@ -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,
}

View file

@ -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": []
}
}
"#;

View file

@ -38,6 +38,7 @@ use webauthn_rs_proto::{
}; };
mod person; mod person;
mod scim;
mod service_account; mod service_account;
mod sync_account; mod sync_account;
mod system; mod system;

16
kanidm_client/src/scim.rs Normal file
View file

@ -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<ScimSyncState, ClientError> {
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
}
}

View file

@ -31,4 +31,21 @@ impl KanidmClient {
self.perform_post_request("/v1/sync_account", new_acct) self.perform_post_request("/v1/sync_account", new_acct)
.await .await
} }
pub async fn idm_sync_account_generate_token(
&self,
id: &str,
label: &str,
) -> Result<String, ClientError> {
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
}
} }

View file

@ -18,6 +18,7 @@ wasm = ["webauthn-rs-proto/wasm"]
[dependencies] [dependencies]
base32.workspace = true base32.workspace = true
base64urlsafedata.workspace = true base64urlsafedata.workspace = true
scim_proto.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
time = { workspace = true, features = ["serde", "std"] } time = { workspace = true, features = ["serde", "std"] }

View file

@ -1,9 +1,155 @@
use base64urlsafedata::Base64UrlSafeData; use base64urlsafedata::Base64UrlSafeData;
use serde::{Deserialize, Serialize}; 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)] #[derive(Serialize, Deserialize, Debug)]
pub enum ScimSyncState { pub enum ScimSyncState {
Initial, Refresh,
Active { cookie: Base64UrlSafeData }, 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<ScimEntry>,
// Delete uuids?
pub delete_uuids: Vec<Uuid>,
}
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<String>,
pub user_name: String,
pub display_name: String,
pub gidnumber: Option<u32>,
pub homedirectory: Option<String>,
pub password_import: Option<String>,
pub login_shell: Option<String>,
}
/*
impl TryFrom<ScimEntry> for ScimSyncPerson {
type Error = ScimError;
fn try_from(_value: ScimEntry) -> Result<Self, Self::Error> {
todo!();
}
}
*/
impl Into<ScimEntry> 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<ScimComplexAttr> 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<String>,
pub name: String,
pub description: Option<String>,
pub gidnumber: Option<u32>,
pub members: Vec<ScimExternalMember>,
}
/*
impl TryFrom<ScimEntry> for ScimSyncPerson {
type Error = ScimError;
fn try_from(_value: ScimEntry) -> Result<Self, Self::Error> {
todo!();
}
}
*/
impl Into<ScimEntry> 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,
}
}
}

View file

@ -2,7 +2,7 @@
name = "kanidm_tools" name = "kanidm_tools"
default-run = "kanidm" default-run = "kanidm"
description = "Kanidm Client Tools" description = "Kanidm Client Tools"
documentation = "https://docs.rs/kanidm_tools/latest/kanidm_tools/" documentation = "https://kanidm.github.io/kanidm/stable/"
version.workspace = true version.workspace = true
authors.workspace = true authors.workspace = true

View file

@ -30,6 +30,7 @@ pub mod raw;
pub mod recycle; pub mod recycle;
pub mod serviceaccount; pub mod serviceaccount;
pub mod session; pub mod session;
pub mod synch;
impl SelfOpt { impl SelfOpt {
pub fn debug(&self) -> bool { pub fn debug(&self) -> bool {
@ -68,6 +69,7 @@ impl SystemOpt {
SystemOpt::PwBadlist { commands } => commands.debug(), SystemOpt::PwBadlist { commands } => commands.debug(),
SystemOpt::Oauth2 { commands } => commands.debug(), SystemOpt::Oauth2 { commands } => commands.debug(),
SystemOpt::Domain { 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::PwBadlist { commands } => commands.exec().await,
SystemOpt::Oauth2 { commands } => commands.exec().await, SystemOpt::Oauth2 { commands } => commands.exec().await,
SystemOpt::Domain { commands } => commands.exec().await, SystemOpt::Domain { commands } => commands.exec().await,
SystemOpt::Synch { commands } => commands.exec().await,
} }
} }
} }

View file

@ -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),
}
}
}
}
}

View file

@ -732,6 +732,42 @@ pub enum DomainOpt {
ResetTokenKey(CommonOpt), 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<String>,
},
#[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)] #[derive(Debug, Subcommand)]
pub enum SystemOpt { pub enum SystemOpt {
#[clap(name = "pw-badlist")] #[clap(name = "pw-badlist")]
@ -752,6 +788,11 @@ pub enum SystemOpt {
#[clap(subcommand)] #[clap(subcommand)]
commands: DomainOpt, commands: DomainOpt,
}, },
#[clap(name = "sync", hide = true)]
Synch {
#[clap(subcommand)]
commands: SynchOpt,
}
} }
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]

View file

@ -37,7 +37,7 @@ use kanidmd_lib::{
// =========================================================== // ===========================================================
pub struct QueryServerReadV1 { pub struct QueryServerReadV1 {
idms: Arc<IdmServer>, pub(crate) idms: Arc<IdmServer>,
ldap: Arc<LdapServer>, ldap: Arc<LdapServer>,
} }

View file

@ -1,9 +1,11 @@
use kanidmd_lib::prelude::*; use kanidmd_lib::prelude::*;
use crate::QueryServerWriteV1; use crate::{QueryServerReadV1, QueryServerWriteV1};
use kanidmd_lib::idm::scim::GenerateScimSyncTokenEvent; use kanidmd_lib::idm::scim::{GenerateScimSyncTokenEvent, ScimSyncUpdateEvent};
use kanidmd_lib::idm::server::IdmServerTransaction; use kanidmd_lib::idm::server::IdmServerTransaction;
use kanidm_proto::scim_v1::{ScimSyncRequest, ScimSyncState};
impl QueryServerWriteV1 { impl QueryServerWriteV1 {
#[instrument( #[instrument(
level = "info", level = "info",
@ -77,4 +79,48 @@ impl QueryServerWriteV1 {
.sync_account_destroy_token(&ident, target, ct) .sync_account_destroy_token(&ident, target, ct)
.and_then(|r| idms_prox_write.commit().map(|_| r)) .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<String>,
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<String>,
eventid: Uuid,
) -> Result<ScimSyncState, OperationError> {
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)
}
} }

View file

@ -72,7 +72,7 @@ pub struct AppState {
pub trait RequestExtensions { pub trait RequestExtensions {
fn get_current_uat(&self) -> Option<String>; fn get_current_uat(&self) -> Option<String>;
fn get_auth_bearer(&self) -> Option<&str>; fn get_auth_bearer(&self) -> Option<String>;
fn get_current_auth_session_id(&self) -> Option<Uuid>; fn get_current_auth_session_id(&self) -> Option<Uuid>;
@ -84,7 +84,7 @@ pub trait RequestExtensions {
} }
impl RequestExtensions for tide::Request<AppState> { impl RequestExtensions for tide::Request<AppState> {
fn get_auth_bearer(&self) -> Option<&str> { fn get_auth_bearer(&self) -> Option<String> {
// Contact the QS to get it to validate wtf is up. // Contact the QS to get it to validate wtf is up.
// let kref = &self.state().bundy_handle; // let kref = &self.state().bundy_handle;
// self.session().get::<UserAuthToken>("uat") // self.session().get::<UserAuthToken>("uat")
@ -97,6 +97,7 @@ impl RequestExtensions for tide::Request<AppState> {
// Turn it to a &str, and then check the prefix // Turn it to a &str, and then check the prefix
h.as_str().strip_prefix("Bearer ") h.as_str().strip_prefix("Bearer ")
}) })
.map(str::to_string)
} }
fn get_current_uat(&self) -> Option<String> { fn get_current_uat(&self) -> Option<String> {

View file

@ -1,5 +1,6 @@
use super::routemaps::{RouteMap, RouteMaps}; use super::routemaps::{RouteMap, RouteMaps};
use super::{to_tide_response, AppState, RequestExtensions}; use super::{to_tide_response, AppState, RequestExtensions};
use kanidm_proto::scim_v1::ScimSyncRequest;
use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::Entry as ProtoEntry;
use kanidmd_lib::prelude::*; use kanidmd_lib::prelude::*;
@ -93,32 +94,36 @@ pub async fn sync_account_token_delete(req: tide::Request<AppState>) -> tide::Re
to_tide_response(res, hvalue) to_tide_response(res, hvalue)
} }
async fn scim_sync_post(_req: tide::Request<AppState>) -> tide::Result { async fn scim_sync_post(mut req: tide::Request<AppState>) -> tide::Result {
// let (eventid, hvalue) = req.new_eventid(); let (eventid, hvalue) = req.new_eventid();
/*
let ApiTokenGenerate {
label,
expiry,
read_write,
} = req.body_json().await?;
*/
// 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<AppState>) -> tide::Result { async fn scim_sync_get(req: tide::Request<AppState>) -> tide::Result {
// let (eventid, hvalue) = req.new_eventid(); 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 let res = req
// What is the connected sync session .state()
// Issue it's current state (version) cookie. .qe_r_ref
.handle_scim_sync_status(bearer, eventid)
// todo!(); .await;
Ok(tide::Response::new(500)) to_tide_response(res, hvalue)
} }
async fn scim_sink_get(req: tide::Request<AppState>) -> tide::Result { async fn scim_sink_get(req: tide::Request<AppState>) -> tide::Result {

View file

@ -1327,7 +1327,8 @@ pub const JSON_IDM_HP_ACP_SYNC_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{
], ],
"acp_modify_presentattr": [ "acp_modify_presentattr": [
"name", "name",
"description" "description",
"sync_token_session"
], ],
"acp_modify_class": [], "acp_modify_class": [],
"acp_create_attr": [ "acp_create_attr": [

View file

@ -1709,7 +1709,7 @@ impl Entry<EntryReduced, EntryCommitted> {
// so they are all in "ldap forms" which makes our next stage a bit easier. // 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. // Stage 1 - transform our results to a map of kani attr -> ldap value.
let attr_map: Result<Map<&str, Vec<String>>, _> = self let attr_map: Result<Map<&str, Vec<Vec<u8>>>, _> = self
.attrs .attrs
.iter() .iter()
.map(|(k, vs)| { .map(|(k, vs)| {
@ -1717,8 +1717,8 @@ impl Entry<EntryReduced, EntryCommitted> {
.map(|pvs| (k.as_str(), pvs)) .map(|pvs| (k.as_str(), pvs))
}) })
.collect(); .collect();
let attr_map = attr_map?; let attr_map = attr_map?;
// Stage 2 - transform and get all our attr - names out that we need to return. // Stage 2 - transform and get all our attr - names out that we need to return.
// ldap a, kani a // ldap a, kani a
let attr_names: Vec<(&str, &str)> = if all_attrs { let attr_names: Vec<(&str, &str)> = if all_attrs {
@ -1748,7 +1748,7 @@ impl Entry<EntryReduced, EntryCommitted> {
match ldap_a { match ldap_a {
"entrydn" => Some(LdapPartialAttribute { "entrydn" => Some(LdapPartialAttribute {
atype: "entrydn".to_string(), atype: "entrydn".to_string(),
vals: vec![dn.clone()], vals: vec![dn.as_bytes().to_vec()],
}), }),
_ => attr_map.get(kani_a).map(|pvs| LdapPartialAttribute { _ => attr_map.get(kani_a).map(|pvs| LdapPartialAttribute {
atype: ldap_a.to_string(), atype: ldap_a.to_string(),

View file

@ -4,6 +4,7 @@ use std::time::Duration;
use base64urlsafedata::Base64UrlSafeData; use base64urlsafedata::Base64UrlSafeData;
use compact_jwt::{Jws, JwsSigner}; use compact_jwt::{Jws, JwsSigner};
use kanidm_proto::scim_v1::ScimSyncRequest;
use kanidm_proto::scim_v1::*; use kanidm_proto::scim_v1::*;
use kanidm_proto::v1::ApiTokenPurpose; use kanidm_proto::v1::ApiTokenPurpose;
use serde::{Deserialize, Serialize}; 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> { impl<'a> IdmServerProxyWriteTransaction<'a> {
pub fn scim_sync_update( pub fn scim_sync_apply(
&mut self, &mut self,
_sse: &ScimSyncUpdateEvent, sse: &ScimSyncUpdateEvent,
_changes: &ScimSyncRequest,
_ct: Duration, _ct: Duration,
) -> Result<(), OperationError> { ) -> 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 // Only update entries related to this uuid
// Make a sync_authority uuid to relate back to on creates. // Make a sync_authority uuid to relate back to on creates.
@ -283,7 +308,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
Some(b) => ScimSyncState::Active { Some(b) => ScimSyncState::Active {
cookie: Base64UrlSafeData(b.to_vec()), cookie: Base64UrlSafeData(b.to_vec()),
}, },
None => ScimSyncState::Initial, None => ScimSyncState::Refresh,
}, },
) )
} }
@ -300,7 +325,7 @@ mod tests {
use kanidm_proto::v1::ApiTokenPurpose; use kanidm_proto::v1::ApiTokenPurpose;
use std::time::Duration; use std::time::Duration;
use super::{GenerateScimSyncTokenEvent, ScimSyncToken}; use super::{GenerateScimSyncTokenEvent, ScimSyncToken, ScimSyncUpdateEvent};
use async_std::task; use async_std::task;
@ -359,7 +384,7 @@ mod tests {
.expect("Failed to get current sync state"); .expect("Failed to get current sync state");
trace!(?sync_state); trace!(?sync_state);
assert!(matches!(sync_state, ScimSyncState::Initial)); assert!(matches!(sync_state, ScimSyncState::Refresh));
drop(idms_prox_read); drop(idms_prox_read);
@ -497,4 +522,141 @@ mod tests {
} }
// Need to delete different phases such as conflictn and end of the agreement. // 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(&gte, 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": []
}
"#;
} }

View file

@ -86,31 +86,31 @@ impl LdapServer {
attributes: vec![ attributes: vec![
LdapPartialAttribute { LdapPartialAttribute {
atype: "objectClass".to_string(), atype: "objectClass".to_string(),
vals: vec!["top".to_string()], vals: vec!["top".as_bytes().to_vec()],
}, },
LdapPartialAttribute { LdapPartialAttribute {
atype: "vendorName".to_string(), atype: "vendorName".to_string(),
vals: vec!["Kanidm Project".to_string()], vals: vec!["Kanidm Project".as_bytes().to_vec()],
}, },
LdapPartialAttribute { LdapPartialAttribute {
atype: "vendorVersion".to_string(), 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 { LdapPartialAttribute {
atype: "supportedLDAPVersion".to_string(), atype: "supportedLDAPVersion".to_string(),
vals: vec!["3".to_string()], vals: vec!["3".as_bytes().to_vec()],
}, },
LdapPartialAttribute { LdapPartialAttribute {
atype: "supportedExtension".to_string(), 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 { LdapPartialAttribute {
atype: "supportedFeatures".to_string(), 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 { LdapPartialAttribute {
atype: "defaultnamingcontext".to_string(), 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(); let mut attrs = HashSet::new();
for a in $e.attributes.iter() { for a in $e.attributes.iter() {
for v in a.vals.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(&( assert!(attrs.contains(&(
$item.0, $item.1 $item.0, $item.1.as_bytes()
))); )));
)* )*

View file

@ -688,22 +688,25 @@ pub trait QueryServerTransaction<'a> {
&self, &self,
value: &ValueSet, value: &ValueSet,
basedn: &str, basedn: &str,
) -> Result<Vec<String>, OperationError> { ) -> Result<Vec<Vec<u8>>, OperationError> {
if let Some(r_set) = value.as_refer_set() { if let Some(r_set) = value.as_refer_set() {
let v: Result<Vec<_>, _> = r_set let v: Result<Vec<_>, _> = r_set
.iter() .iter()
.copied() .copied()
.map(|ur| { .map(|ur| {
let rdn = self.uuid_to_rdn(ur)?; let rdn = self.uuid_to_rdn(ur)?;
Ok(format!("{},{}", rdn, basedn)) Ok(format!("{},{}", rdn, basedn).into_bytes())
}) })
.collect(); .collect();
v v
} else if let Some(k_set) = value.as_sshkey_map() { } 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) Ok(v)
} else { } 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) Ok(v)
} }
} }

View file

@ -97,11 +97,14 @@ impl DirectoryServer {
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: "objectClass".to_string(),
vals: vec!["top".to_string(), "organizationalUnit".to_string()], vals: vec![
"top".as_bytes().into(),
"organizationalUnit".as_bytes().into(),
],
}, },
LdapAttribute { LdapAttribute {
atype: "ou".to_string(), atype: "ou".to_string(),
vals: vec!["people".to_string()], vals: vec!["people".as_bytes().into()],
}, },
], ],
}; };
@ -121,11 +124,14 @@ impl DirectoryServer {
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: "objectClass".to_string(),
vals: vec!["top".to_string(), "organizationalUnit".to_string()], vals: vec![
"top".as_bytes().into(),
"organizationalUnit".as_bytes().into(),
],
}, },
LdapAttribute { LdapAttribute {
atype: "ou".to_string(), atype: "ou".to_string(),
vals: vec!["groups".to_string()], vals: vec!["groups".as_bytes().into()],
}, },
], ],
}; };
@ -155,40 +161,40 @@ impl DirectoryServer {
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: "objectClass".to_string(),
vals: vec![ vals: vec![
"top".to_string(), "top".as_bytes().into(),
"nsPerson".to_string(), "nsPerson".as_bytes().into(),
"nsAccount".to_string(), "nsAccount".as_bytes().into(),
"nsOrgPerson".to_string(), "nsOrgPerson".as_bytes().into(),
"posixAccount".to_string(), "posixAccount".as_bytes().into(),
], ],
}, },
LdapAttribute { LdapAttribute {
atype: "cn".to_string(), atype: "cn".to_string(),
vals: vec![a.uuid.to_string()], vals: vec![a.uuid.as_bytes().to_vec()],
}, },
LdapAttribute { LdapAttribute {
atype: "uid".to_string(), atype: "uid".to_string(),
vals: vec![a.name.clone()], vals: vec![a.name.as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "displayName".to_string(), atype: "displayName".to_string(),
vals: vec![a.display_name.clone()], vals: vec![a.display_name.as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "userPassword".to_string(), atype: "userPassword".to_string(),
vals: vec![a.password.clone()], vals: vec![a.password.as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "homeDirectory".to_string(), atype: "homeDirectory".to_string(),
vals: vec![format!("/home/{}", a.uuid)], vals: vec![format!("/home/{}", a.uuid).as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "uidNumber".to_string(), atype: "uidNumber".to_string(),
vals: vec!["1000".to_string()], vals: vec!["1000".as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "gidNumber".to_string(), atype: "gidNumber".to_string(),
vals: vec!["1000".to_string()], vals: vec!["1000".as_bytes().into()],
}, },
], ],
}; };
@ -200,11 +206,14 @@ impl DirectoryServer {
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: "objectClass".to_string(),
vals: vec!["top".to_string(), "groupOfNames".to_string()], vals: vec![
"top".as_bytes().into(),
"groupOfNames".as_bytes().into(),
],
}, },
LdapAttribute { LdapAttribute {
atype: "cn".to_string(), 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 // List of dns
let vals: Vec<_> = g let vals: Vec<Vec<u8>> = g
.members .members
.iter() .iter()
.map(|id| { .map(|id| {
@ -230,6 +239,8 @@ impl DirectoryServer {
.get(id) .get(id)
.unwrap() .unwrap()
.get_ds_ldap_dn(&self.ldap.basedn) .get_ds_ldap_dn(&self.ldap.basedn)
.as_bytes()
.into()
}) })
.collect(); .collect();
@ -270,11 +281,11 @@ impl DirectoryServer {
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: "objectClass".to_string(),
vals: vec!["top".to_string(), "groupOfNames".to_string()], vals: vec!["top".as_bytes().into(), "groupOfNames".as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "cn".to_string(), 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![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: "objectClass".to_string(),
vals: vec!["top".to_string(), "groupOfNames".to_string()], vals: vec!["top".as_bytes().into(), "groupOfNames".as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "cn".to_string(), 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 { modification: LdapPartialAttribute {
atype: "aci".to_string(), atype: "aci".to_string(),
vals: vec![ 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="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");)"#.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");)"#.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), 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");)"#.to_string(), 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");)"#.to_string(), 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), 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; == 0;
if need_account { 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 { 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(),
)
} }
} }