diff --git a/Makefile b/Makefile index 8a045b4cb..e8a1058e0 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,6 @@ CONTAINER_TOOL_ARGS ?= IMAGE_ARCH ?= "linux/amd64,linux/arm64" CONTAINER_BUILD_ARGS ?= MARKDOWN_FORMAT_ARGS ?= --options-line-width=100 -# Example of using redis with sccache -# --build-arg "SCCACHE_REDIS=redis://redis.dev.blackhats.net.au:6379" CONTAINER_TOOL ?= docker BOOK_VERSION ?= master diff --git a/README.md b/README.md index c376d813a..a46c83343 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,9 @@ of resource overhead and difficulty for administration. Kanidm aims to have the features richness of FreeIPA, but without the resource and administration overheads. If you want a complete IDM package, but in a lighter footprint and easier to manage, then -Kanidm is probably for you. +Kanidm is probably for you. In testing with 3000 users + 1500 groups, Kanidm is 3 times faster +for search operations and 5 times faster for modification and addition of entries (your results may +differ however, but generally Kanidm is much faster than FreeIPA). ## Developer Getting Started diff --git a/kanidm_tools/Dockerfile b/kanidm_tools/Dockerfile index a002e5647..4558c9599 100644 --- a/kanidm_tools/Dockerfile +++ b/kanidm_tools/Dockerfile @@ -19,7 +19,6 @@ RUN zypper install -y \ pam-devel \ libudev-devel \ sqlite3-devel \ - sccache \ rsync RUN zypper clean -a RUN rustup default stable @@ -34,17 +33,6 @@ RUN echo Features $KANIDM_FEATURES ENV CARGO_HOME=/scratch/.cargo ENV RUSTFLAGS="-Clinker=clang -Clink-arg=-fuse-ld=/usr/bin/ld.lld" -# set up sccache if you've done the thing -RUN if [ "${SCCACHE_REDIS}" != "" ]; \ -then \ - export CARGO_INCREMENTAL=false && \ - export CC="/usr/bin/sccache /usr/bin/clang" && \ - export RUSTC_WRAPPER=sccache && \ - sccache --start-server; \ -else \ - export CC="/usr/bin/clang"; \ -fi - WORKDIR /usr/src/kanidm/ # build the CLI RUN if [ -z "${KANIDM_FEATURES}" ]; then \ @@ -65,8 +53,6 @@ else \ --release; \ fi -RUN if [ "${SCCACHE_REDIS}" != "" ]; then sccache -s; fi - RUN ls -al /usr/src/kanidm/target/release # == Construct the tools container diff --git a/kanidmd/Dockerfile b/kanidmd/Dockerfile index dee69c93e..0f0125a7f 100644 --- a/kanidmd/Dockerfile +++ b/kanidmd/Dockerfile @@ -13,7 +13,6 @@ RUN zypper install -y \ make automake autoconf \ libopenssl-devel pam-devel \ sqlite3-devel \ - sccache \ gcc \ rsync \ findutils \ @@ -23,7 +22,6 @@ RUN rustup default stable COPY . /usr/src/kanidm -ARG SCCACHE_REDIS="" ARG KANIDM_FEATURES ARG KANIDM_BUILD_PROFILE="container_generic" ARG KANIDM_BUILD_OPTIONS="" @@ -32,21 +30,14 @@ RUN mkdir /scratch RUN echo $KANIDM_BUILD_PROFILE RUN echo $KANIDM_FEATURES - ENV CARGO_HOME=/scratch/.cargo # ====================== -# WORKDIR /usr/src/kanidm/kanidmd_web_ui +WORKDIR /usr/src/kanidm/kanidmd_web_ui # # This can't be used in the wasm build for now. # # ENV RUSTFLAGS="-Clinker=clang" -# RUN if [ "${SCCACHE_REDIS}" != "" ]; \ -# then \ -# export CARGO_INCREMENTAL=false && \ -# export RUSTC_WRAPPER=sccache && \ -# sccache --start-server; \ -# fi && \ -# ./build_wasm.sh +RUN ./build_wasm.sh # ====================== @@ -57,15 +48,7 @@ ENV KANIDM_BUILD_PROFILE="${KANIDM_BUILD_PROFILE}" ENV RUSTFLAGS="-Clinker=clang -Clink-arg=-fuse-ld=/usr/bin/ld.lld" # Exports don't persist through RUN statements. -RUN if [ "${SCCACHE_REDIS}" != "" ]; \ -then \ - export CARGO_INCREMENTAL=false && \ - export CC="/usr/bin/sccache /usr/bin/clang" && \ - export RUSTC_WRAPPER=sccache && \ - sccache --start-server; \ -else \ - export CC="/usr/bin/clang"; \ -fi && \ +RUN export CC="/usr/bin/clang"; \ if [ -z "${KANIDM_FEATURES}" ]; then \ cargo build -p daemon ${KANIDM_BUILD_OPTIONS} \ --target-dir="/usr/src/kanidm/target/" \ @@ -75,8 +58,7 @@ else \ --target-dir="/usr/src/kanidm/target/" \ --features="${KANIDM_FEATURES}" \ --release; \ -fi && \ -if [ "${SCCACHE_REDIS}" != "" ]; then sccache -s; fi +fi RUN ls -al /usr/src/kanidm/target/release diff --git a/orca/src/data.rs b/orca/src/data.rs index 665b1994d..b9d5f4862 100644 --- a/orca/src/data.rs +++ b/orca/src/data.rs @@ -46,6 +46,10 @@ impl Account { format!("uid={},ou=people,{}", self.name.as_str(), basedn) } + pub fn get_ipa_ldap_dn(&self, basedn: &str) -> String { + format!("uid={},cn=users,cn=accounts,{}", self.name.as_str(), basedn) + } + pub fn generate(uuid: Uuid) -> Self { let mut rng = rand::thread_rng(); let id: u64 = rng.gen(); @@ -73,6 +77,10 @@ impl Group { format!("cn={},ou=groups,{}", self.name.as_str(), basedn) } + pub fn get_ipa_ldap_dn(&self, basedn: &str) -> String { + format!("cn={},cn=groups,cn=accounts,{}", self.name.as_str(), basedn) + } + pub fn generate(uuid: Uuid, members: Vec) -> Self { let mut rng = rand::thread_rng(); @@ -115,6 +123,13 @@ impl Entity { } } + pub fn get_ipa_ldap_dn(&self, basedn: &str) -> String { + match self { + Entity::Account(a) => a.get_ipa_ldap_dn(basedn), + Entity::Group(g) => g.get_ipa_ldap_dn(basedn), + } + } + pub fn get_entity_type(&self) -> EntityType { match self { Entity::Account(a) => EntityType::Account(a.uuid), diff --git a/orca/src/generate.rs b/orca/src/generate.rs new file mode 100644 index 000000000..bf10f2f7d --- /dev/null +++ b/orca/src/generate.rs @@ -0,0 +1,163 @@ +use std::fs::File; +use std::path::Path; +use std::time::Duration; +use uuid::Uuid; + +use std::collections::{HashMap, HashSet}; + +use crate::data::*; + +const N_USERS: usize = 3000; +const N_GROUPS: usize = 1500; +const N_MEMBERSHIPS: usize = 10; +const N_NEST: usize = 4; + +pub(crate) fn doit(output: &Path) -> () { + info!( + "Performing data generation into {}", + output.to_str().unwrap(), + ); + + let mut rng = rand::thread_rng(); + + if N_MEMBERSHIPS >= N_GROUPS { + error!("Too many memberships per group. Memberships must be less that n-groups"); + return; + } + + // Open before we start so we have it ready to go. + let out_file = match File::create(output) { + Ok(f) => f, + Err(e) => { + error!("Failed to open {} - {:?}", output.to_str().unwrap(), e); + return; + } + }; + + // Number of users + let accounts: Vec<_> = (0..N_USERS) + .map(|i| Account { + name: format!("testuser{}", i), + display_name: format!("Test User {}", i), + password: readable_password_from_random(), + uuid: Uuid::new_v4(), + }) + .collect(); + + // Number of groups. + let mut groups: Vec<_> = (0..N_GROUPS) + .map(|i| Group { + name: format!("testgroup{}", i), + uuid: Uuid::new_v4(), + members: Vec::new(), + }) + .collect(); + + // Should groups be randomly nested? + // The way this is done is we split the array based on nest level. If it's 1, we split + // in 2, 2 we split in 3 and so on. + if N_NEST > 0 { + debug!("Nesting Groups"); + + let chunk_size = N_GROUPS / (N_NEST + 1); + if chunk_size == 0 { + error!("Unable to chunk groups, need (N_GROUPS / (N_NEST + 1)) > 0"); + return; + } + + let mut chunk_iter = groups.chunks_mut(chunk_size); + // Can't fail due to above checks. + let mut p_chunk = chunk_iter.next().unwrap(); + while let Some(w_chunk) = chunk_iter.next() { + // add items from work chunk to parent chunk + p_chunk + .iter_mut() + .zip(w_chunk.iter()) + .for_each(|(p, w): (&mut _, &_)| p.members.push(w.uuid)); + + // swap w_chunk to p_chunk + p_chunk = w_chunk; + } + } + + // Number of memberships per user. + // We use rand for this to sample random numbers of + for acc in accounts.iter() { + // Sample randomly. + for idx in rand::seq::index::sample(&mut rng, N_GROUPS, N_MEMBERSHIPS).iter() { + groups[idx].members.push(acc.uuid); + } + } + + // Build from the generated data above. + let all_entities: HashMap = accounts + .into_iter() + .map(|acc| (acc.uuid, Entity::Account(acc))) + .chain(groups.into_iter().map(|grp| (grp.uuid, Entity::Group(grp)))) + .collect(); + + // Define the entries that should exist "at the start of the test". For now, we just + // create everything. Maybe when we start to add mod tests we need to retain a pool + // of things to retain here for those ops. + let precreate: HashSet<_> = all_entities.keys().copied().collect(); + + // The set of accounts in all_entities. + let accounts: HashSet = all_entities + .iter() + .filter_map(|(uuid, ent)| match ent { + Entity::Account(_) => Some(*uuid), + _ => None, + }) + .collect(); + + // This defines a map of "entity" to "what can it manipulate". This + // is used to create access controls in some cases for mod tests. + // + // For example, if we have user with uuid X and it changes Group with + // uuid Y, then we need to ensure that X has group-mod permissions over + // Y in some capacity. + let access: HashMap> = HashMap::new(); + + // The set of operations to simulate. We pre-calc these so tests can randomly + // sample and perform the searches as needed. + + // We don't have original times, so we can fudge these. + let orig_etime = Duration::from_secs(1); + let rtime = Duration::from_secs(1); + // Needed for random sampling. + let all_ids: Vec<_> = all_entities.keys().copied().collect(); + let all_ids_len = all_ids.len(); + + let connections: Vec<_> = (0..all_ids_len) + .map(|id| { + // Could be rand? + let n_search = 1; + + let mut search_ids = Vec::new(); + for idx in rand::seq::index::sample(&mut rng, all_ids_len, n_search).iter() { + search_ids.push(all_ids[idx]); + } + // + Conn { + id: id as i32, + ops: vec![Op { + orig_etime, + rtime, + op_type: OpType::Search(search_ids), + }], + } + }) + .collect(); + + let td = TestData { + all_entities, + access, + accounts, + precreate, + connections, + }; + + if let Err(e) = serde_json::to_writer_pretty(out_file, &td) { + error!("Writing to file -> {:?}", e); + }; +} diff --git a/orca/src/ipa.rs b/orca/src/ipa.rs new file mode 100644 index 000000000..7996c1091 --- /dev/null +++ b/orca/src/ipa.rs @@ -0,0 +1,295 @@ +use ldap3_proto::proto::*; +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, Instant}; +use uuid::Uuid; + +use crate::data::*; +use crate::ldap::{LdapClient, LdapSchema}; +use crate::profile::IpaConfig; +use crate::{TargetServer, TargetServerBuilder}; + +#[derive(Debug)] +pub struct IpaServer { + ldap: LdapClient, + realm: String, + admin_pw: String, +} + +impl IpaServer { + fn construct(uri: String, realm: String, admin_pw: String) -> Result { + // explode the realm to basedn. + // dev.kanidm.com + // dc=dev,dc=kanidm,dc=com + let basedn = format!("dc={}", realm.replace('.', ",dc=")); + + let ldap = LdapClient::new(uri, basedn, LdapSchema::Rfc2307bis)?; + + Ok(IpaServer { + ldap, + realm, + admin_pw, + }) + } + + pub fn build(uri: String, realm: String, admin_pw: String) -> Result { + Self::construct(uri, realm, admin_pw).map(TargetServer::Ipa) + } + + #[allow(clippy::new_ret_no_self)] + pub fn new(lconfig: &IpaConfig) -> Result { + Self::construct( + lconfig.uri.clone(), + lconfig.realm.clone(), + lconfig.admin_pw.clone(), + ) + .map(TargetServer::Ipa) + } + + pub fn info(&self) -> String { + format!("Ipa Server Connection: {} @ {}", self.realm, self.ldap.uri) + } + + pub fn builder(&self) -> TargetServerBuilder { + TargetServerBuilder::Ipa( + self.ldap.uri.clone(), + self.realm.clone(), + self.admin_pw.clone(), + ) + } + + pub async fn open_admin_connection(&self) -> Result<(), ()> { + self.ldap.open_ipa_admin_connection(&self.admin_pw).await + } + + pub async fn setup_admin_delete_uuids(&self, _targets: &[Uuid]) -> Result<(), ()> { + // todo!(); + Ok(()) + } + + pub async fn setup_admin_precreate_entities( + &self, + targets: &HashSet, + all_entities: &HashMap, + ) -> Result<(), ()> { + for u in targets { + let e = all_entities.get(u).unwrap(); + // does it already exist? + let res = self + .ldap + .search(LdapFilter::Equality( + "cn".to_string(), + e.get_name().to_string(), + )) + .await?; + + if !res.is_empty() { + continue; + } + + let dn = e.get_ipa_ldap_dn(&self.ldap.basedn); + match e { + Entity::Account(a) => { + let account = LdapAddRequest { + dn, + attributes: vec![ + LdapAttribute { + atype: "objectClass".to_string(), + vals: vec![ + "ipaobject".as_bytes().into(), + "person".as_bytes().into(), + "top".as_bytes().into(), + "ipasshuser".as_bytes().into(), + "inetorgperson".as_bytes().into(), + "organizationalperson".as_bytes().into(), + "krbticketpolicyaux".as_bytes().into(), + "krbprincipalaux".as_bytes().into(), + "inetuser".as_bytes().into(), + "posixaccount".as_bytes().into(), + "meporiginentry".as_bytes().into(), + ], + }, + LdapAttribute { + atype: "ipauniqueid".to_string(), + vals: vec!["autogenerate".as_bytes().into()], + }, + LdapAttribute { + atype: "uid".to_string(), + vals: vec![a.name.as_bytes().into()], + }, + LdapAttribute { + atype: "cn".to_string(), + vals: vec![a.name.as_bytes().into()], + }, + LdapAttribute { + atype: "givenName".to_string(), + vals: vec![a.name.as_bytes().into()], + }, + LdapAttribute { + atype: "sn".to_string(), + vals: vec![a.name.as_bytes().into()], + }, + LdapAttribute { + atype: "displayName".to_string(), + vals: vec![a.display_name.as_bytes().into()], + }, + LdapAttribute { + atype: "gecos".to_string(), + vals: vec![a.display_name.as_bytes().into()], + }, + LdapAttribute { + atype: "userPassword".to_string(), + vals: vec![a.password.as_bytes().into()], + }, + LdapAttribute { + atype: "initials".to_string(), + vals: vec!["tu".as_bytes().into()], + }, + LdapAttribute { + atype: "homeDirectory".to_string(), + vals: vec![format!("/home/{}", a.name).as_bytes().into()], + }, + LdapAttribute { + atype: "mail".to_string(), + vals: vec![format!("{}@{}", a.name, self.realm).as_bytes().into()], + }, + LdapAttribute { + atype: "loginshell".to_string(), + vals: vec!["/bin/zsh".as_bytes().into()], + }, + LdapAttribute { + atype: "uidNumber".to_string(), + vals: vec!["-1".as_bytes().into()], + }, + LdapAttribute { + atype: "gidNumber".to_string(), + vals: vec!["-1".as_bytes().into()], + }, + LdapAttribute { + atype: "krbextradata".to_string(), + vals: vec!["placeholder".as_bytes().into()], + }, + LdapAttribute { + atype: "krblastpwdchange".to_string(), + vals: vec!["20230119053224Z".as_bytes().into()], + }, + LdapAttribute { + atype: "krbPasswordExpiration".to_string(), + vals: vec!["20380119053224Z".as_bytes().into()], + }, + LdapAttribute { + atype: "krbPrincipalName".to_string(), + vals: vec![format!("{}@{}", a.name, self.realm.to_uppercase()) + .as_bytes() + .into()], + }, + LdapAttribute { + atype: "krbCanonicalName".to_string(), + vals: vec![format!("{}@{}", a.name, self.realm.to_uppercase()) + .as_bytes() + .into()], + }, + ], + }; + self.ldap.add(account).await?; + } + Entity::Group(g) => { + let group = LdapAddRequest { + dn, + attributes: vec![ + LdapAttribute { + atype: "objectClass".to_string(), + vals: vec![ + "top".as_bytes().into(), + "groupofnames".as_bytes().into(), + "nestedgroup".as_bytes().into(), + "ipausergroup".as_bytes().into(), + "ipaobject".as_bytes().into(), + "posixgroup".as_bytes().into(), + ], + }, + LdapAttribute { + atype: "cn".to_string(), + vals: vec![g.name.as_bytes().into()], + }, + LdapAttribute { + atype: "ipauniqueid".to_string(), + vals: vec!["autogenerate".as_bytes().into()], + }, + LdapAttribute { + atype: "gidNumber".to_string(), + vals: vec!["-1".as_bytes().into()], + }, + ], + }; + self.ldap.add(group).await?; + } + } + } + + // Add all the members. + for g in targets.iter().filter_map(|u| { + let e = all_entities.get(u).unwrap(); + match e { + Entity::Group(g) => Some(g), + _ => None, + } + }) { + // List of dns + let vals: Vec> = g + .members + .iter() + .map(|id| { + all_entities + .get(id) + .unwrap() + .get_ipa_ldap_dn(&self.ldap.basedn) + .as_bytes() + .into() + }) + .collect(); + + let req = LdapModifyRequest { + dn: g.get_ipa_ldap_dn(&self.ldap.basedn), + changes: vec![LdapModify { + operation: LdapModifyType::Replace, + modification: LdapPartialAttribute { + atype: "member".to_string(), + vals, + }, + }], + }; + self.ldap.modify(req).await?; + } + Ok(()) + } + + pub async fn setup_access_controls( + &self, + _access: &HashMap>, + _all_entities: &HashMap, + ) -> Result<(), ()> { + // todo!(); + Ok(()) + } + + pub async fn open_user_connection( + &self, + test_start: Instant, + name: &str, + pw: &str, + ) -> Result<(Duration, Duration), ()> { + self.ldap.open_user_connection(test_start, name, pw).await + } + + pub async fn close_connection(&self) { + self.ldap.close_connection().await; + } + + pub async fn search( + &self, + test_start: Instant, + ids: &[String], + ) -> Result<(Duration, Duration, usize), ()> { + self.ldap.search_name(test_start, ids).await + } +} diff --git a/orca/src/kani.rs b/orca/src/kani.rs index 1be1402b9..480c573a3 100644 --- a/orca/src/kani.rs +++ b/orca/src/kani.rs @@ -291,7 +291,12 @@ impl KaniHttpServer { } pub async fn close_connection(&self) { - assert!(self.client.logout().await.is_ok()); + assert!(self + .client + .logout() + .await + .map_err(|e| error!("close_connection {:?}", e)) + .is_ok()); } pub async fn search( diff --git a/orca/src/ldap.rs b/orca/src/ldap.rs index 1079ac54b..ca1d13cd2 100644 --- a/orca/src/ldap.rs +++ b/orca/src/ldap.rs @@ -135,6 +135,11 @@ impl LdapClient { .await } + pub async fn open_ipa_admin_connection(&self, pw: &str) -> Result<(), ()> { + let admin_dn = format!("uid=admin,cn=users,cn=accounts,{}", self.basedn); + self.bind(admin_dn, pw.to_string()).await + } + pub async fn open_user_connection( &self, test_start: Instant, diff --git a/orca/src/main.rs b/orca/src/main.rs index db291551d..2a1e31e55 100644 --- a/orca/src/main.rs +++ b/orca/src/main.rs @@ -16,17 +16,21 @@ static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; extern crate tracing; use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use clap::{Parser, Subcommand}; use uuid::Uuid; use crate::ds::DirectoryServer; +use crate::ipa::IpaServer; use crate::kani::{KaniHttpServer, KaniLdapServer}; +use crate::setup::config; mod data; mod ds; +mod generate; +mod ipa; mod kani; mod ldap; mod preprocess; @@ -39,6 +43,8 @@ include!("./opt.rs"); impl OrcaOpt { pub fn debug(&self) -> bool { match self { + OrcaOpt::TestConnection(opt) => opt.copt.debug, + OrcaOpt::Generate(opt) => opt.copt.debug, OrcaOpt::PreProc(opt) => opt.copt.debug, OrcaOpt::Setup(opt) => opt.copt.debug, OrcaOpt::Run(opt) => opt.copt.debug, @@ -50,6 +56,7 @@ pub enum TargetServerBuilder { Kanidm(String, String), KanidmLdap(String, String, String, String), DirSrv(String, String, String), + Ipa(String, String, String), } impl TargetServerBuilder { @@ -59,6 +66,7 @@ impl TargetServerBuilder { TargetServerBuilder::Kanidm(a, b) => KaniHttpServer::build(a, b), TargetServerBuilder::KanidmLdap(a, b, c, d) => KaniLdapServer::build(a, b, c, d), TargetServerBuilder::DirSrv(a, b, c) => DirectoryServer::build(a, b, c), + TargetServerBuilder::Ipa(a, b, c) => IpaServer::build(a, b, c), } } } @@ -68,6 +76,7 @@ pub enum TargetServer { Kanidm(KaniHttpServer), KanidmLdap(Box), DirSrv(DirectoryServer), + Ipa(IpaServer), } impl TargetServer { @@ -76,6 +85,7 @@ impl TargetServer { TargetServer::Kanidm(k) => k.info(), TargetServer::KanidmLdap(k) => k.info(), TargetServer::DirSrv(k) => k.info(), + TargetServer::Ipa(k) => k.info(), } } @@ -84,6 +94,7 @@ impl TargetServer { TargetServer::Kanidm(_) => "kanidm_http", TargetServer::KanidmLdap(_) => "kanidm_ldap", TargetServer::DirSrv(_) => "directory_server", + TargetServer::Ipa(_) => "ipa", } } @@ -92,6 +103,7 @@ impl TargetServer { TargetServer::Kanidm(k) => k.builder(), TargetServer::KanidmLdap(k) => k.builder(), TargetServer::DirSrv(k) => k.builder(), + TargetServer::Ipa(k) => k.builder(), } } @@ -100,6 +112,7 @@ impl TargetServer { TargetServer::Kanidm(k) => k.open_admin_connection().await, TargetServer::KanidmLdap(k) => k.open_admin_connection().await, TargetServer::DirSrv(k) => k.open_admin_connection().await, + TargetServer::Ipa(k) => k.open_admin_connection().await, } } @@ -108,6 +121,7 @@ impl TargetServer { TargetServer::Kanidm(k) => k.setup_admin_delete_uuids(targets).await, TargetServer::KanidmLdap(k) => k.setup_admin_delete_uuids(targets).await, TargetServer::DirSrv(k) => k.setup_admin_delete_uuids(targets).await, + TargetServer::Ipa(k) => k.setup_admin_delete_uuids(targets).await, } } @@ -129,6 +143,10 @@ impl TargetServer { k.setup_admin_precreate_entities(targets, all_entities) .await } + TargetServer::Ipa(k) => { + k.setup_admin_precreate_entities(targets, all_entities) + .await + } } } @@ -141,6 +159,7 @@ impl TargetServer { TargetServer::Kanidm(k) => k.setup_access_controls(access, all_entities).await, TargetServer::KanidmLdap(k) => k.setup_access_controls(access, all_entities).await, TargetServer::DirSrv(k) => k.setup_access_controls(access, all_entities).await, + TargetServer::Ipa(k) => k.setup_access_controls(access, all_entities).await, } } @@ -154,6 +173,7 @@ impl TargetServer { TargetServer::Kanidm(k) => k.open_user_connection(test_start, name, pw).await, TargetServer::KanidmLdap(k) => k.open_user_connection(test_start, name, pw).await, TargetServer::DirSrv(k) => k.open_user_connection(test_start, name, pw).await, + TargetServer::Ipa(k) => k.open_user_connection(test_start, name, pw).await, } } @@ -162,6 +182,7 @@ impl TargetServer { TargetServer::Kanidm(k) => k.close_connection().await, TargetServer::KanidmLdap(k) => k.close_connection().await, TargetServer::DirSrv(k) => k.close_connection().await, + TargetServer::Ipa(k) => k.close_connection().await, } } @@ -174,10 +195,27 @@ impl TargetServer { TargetServer::Kanidm(k) => k.search(test_start, ids).await, TargetServer::KanidmLdap(k) => k.search(test_start, ids).await, TargetServer::DirSrv(k) => k.search(test_start, ids).await, + TargetServer::Ipa(k) => k.search(test_start, ids).await, } } } +async fn conntest(target: &TargetOpt, profile_path: &Path) -> Result<(), ()> { + info!( + "Performing conntest of {:?} from {}", + target, + profile_path.to_str().unwrap(), + ); + + let (_data, _profile, server) = config(target, profile_path)?; + + server + .open_admin_connection() + .await + .map(|_| info!("success")) + .map_err(|_| error!("connection test failed")) +} + #[tokio::main] async fn main() { let opt = OrcaOpt::parse(); @@ -193,6 +231,10 @@ async fn main() { info!("Orca - the Kanidm Load Testing Utility."); debug!("cli -> {:?}", opt); match opt { + OrcaOpt::TestConnection(opt) => { + let _ = conntest(&opt.target, &opt.profile_path).await; + } + OrcaOpt::Generate(opt) => generate::doit(&opt.output_path), OrcaOpt::PreProc(opt) => preprocess::doit(&opt.input_path, &opt.output_path), OrcaOpt::Setup(opt) => { let _ = setup::doit(&opt.target, &opt.profile_path).await; @@ -204,5 +246,5 @@ async fn main() { // run the test! } }; - info!("Success"); + debug!("Exit"); } diff --git a/orca/src/opt.rs b/orca/src/opt.rs index eccf9d687..6a73a74df 100644 --- a/orca/src/opt.rs +++ b/orca/src/opt.rs @@ -19,13 +19,20 @@ struct PreProcOpt { pub output_path: PathBuf, } +#[derive(Debug, Parser)] +struct GenerateOpt { + #[clap(flatten)] + pub copt: CommonOpt, + #[clap(parse(from_os_str), short, long = "output")] + /// Path to write the generated output. + pub output_path: PathBuf, +} + #[derive(Debug, Parser)] struct SetupOpt { #[clap(flatten)] pub copt: CommonOpt, #[clap(name = "target")] - /// Which service to target during this operation. - /// Valid values are "ds" or "kanidm" pub target: TargetOpt, #[clap(parse(from_os_str), short, long = "profile")] /// Path to the test profile. @@ -37,8 +44,6 @@ struct RunOpt { #[clap(flatten)] pub copt: CommonOpt, #[clap(name = "target")] - /// Which service to target during this operation. - /// Valid values are "ds" or "kanidm" pub target: TargetOpt, #[clap(name = "test_type")] /// Which type of test to run against this system @@ -49,10 +54,14 @@ struct RunOpt { } #[derive(Debug, Subcommand)] +/// The target to run against pub(crate) enum TargetOpt { #[clap(name = "ds")] /// Run against the ldap/ds profile Ds, + #[clap(name = "ipa")] + /// Run against the ipa profile + Ipa, #[clap(name = "kanidm")] /// Run against the kanidm http profile Kanidm, @@ -67,9 +76,10 @@ impl FromStr for TargetOpt { fn from_str(s: &str) -> Result { match s { "ds" => Ok(TargetOpt::Ds), + "ipa" => Ok(TargetOpt::Ipa), "kanidm" => Ok(TargetOpt::Kanidm), "kanidm_ldap" => Ok(TargetOpt::KanidmLdap), - _ => Err("Invalid target type. Must be ds, kanidm, or kanidm_ldap"), + _ => Err("Invalid target type. Must be ds, ipa, kanidm, or kanidm_ldap"), } } } @@ -122,6 +132,13 @@ Orca works in a few steps. " )] enum OrcaOpt { + #[clap(name = "conntest")] + /// Perform a connection test against the specified target + TestConnection(SetupOpt), + #[clap(name = "generate")] + /// Generate a new dataset that can be used for testing. Parameters can be provided + /// to affect the type and quantity of data created. + Generate(GenerateOpt), #[clap(name = "preprocess")] /// Preprocess a dataset that can be used for testing PreProc(PreProcOpt), diff --git a/orca/src/profile.rs b/orca/src/profile.rs index 5bf8bbc09..abf26f0fd 100644 --- a/orca/src/profile.rs +++ b/orca/src/profile.rs @@ -7,6 +7,13 @@ pub struct DsConfig { pub base_dn: String, } +#[derive(Debug, Deserialize)] +pub struct IpaConfig { + pub uri: String, + pub realm: String, + pub admin_pw: String, +} + #[derive(Debug, Deserialize)] pub struct KaniHttpConfig { pub uri: String, @@ -43,6 +50,7 @@ pub struct Profile { pub data: String, pub results: String, pub ds_config: Option, + pub ipa_config: Option, pub kani_http_config: Option, pub kani_ldap_config: Option, #[serde(default)] diff --git a/orca/src/runner/search.rs b/orca/src/runner/search.rs index 2122442c6..3b9fdeac4 100644 --- a/orca/src/runner/search.rs +++ b/orca/src/runner/search.rs @@ -76,7 +76,6 @@ async fn basic_arbiter( loop { match raw_results_rx.recv_deadline(end_of_test) { - // We are currently discarding results. Ok(datum) => results.push(datum), Err(RecvTimeoutError::Timeout) => { break; diff --git a/orca/src/setup.rs b/orca/src/setup.rs index 2bb23bcd6..7585818eb 100644 --- a/orca/src/setup.rs +++ b/orca/src/setup.rs @@ -6,6 +6,7 @@ use uuid::Uuid; use crate::data::TestData; use crate::ds::DirectoryServer; +use crate::ipa::IpaServer; use crate::kani::{KaniHttpServer, KaniLdapServer}; use crate::profile::Profile; use crate::{TargetOpt, TargetServer}; @@ -54,6 +55,14 @@ pub(crate) fn config( return Err(()); } } + TargetOpt::Ipa => { + if let Some(ipaconfig) = profile.ipa_config.as_ref() { + IpaServer::new(ipaconfig)? + } else { + error!("To use ipa, you must have the ipa_config section in your profile"); + return Err(()); + } + } TargetOpt::KanidmLdap => { if let Some(klconfig) = profile.kani_ldap_config.as_ref() { KaniLdapServer::new(klconfig)?