From 3bfc347c53ed307a61ad9077ce570adc88aa3bc9 Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Mon, 30 Oct 2023 16:10:54 +1000 Subject: [PATCH] CLI integration test beginnings (#2261) * more integration test things, using assert_cmd to test the CLI end-to-end * packagez * making clippy happy * making deno happy --- .github/workflows/rust_build.yml | 1 + Cargo.lock | 100 +++++++++++- book/src/integrations/pam_and_nsswitch.md | 8 +- libs/client/src/lib.rs | 68 +++++---- libs/sketching/src/lib.rs | 27 +++- proto/src/constants.rs | 3 + scripts/oauth_proxy/README.md | 3 +- server/daemon/src/opt.rs | 13 +- server/testkit-macros/src/lib.rs | 31 ++++ server/testkit/Cargo.toml | 40 +++-- server/testkit/src/lib.rs | 2 + server/testkit/tests/integration.rs | 144 +++++++++++++++++- tools/cli/Cargo.toml | 2 + tools/cli/src/cli/common.rs | 12 +- tools/cli/src/cli/domain.rs | 10 +- tools/cli/src/cli/group/account_policy.rs | 6 +- tools/cli/src/cli/group/mod.rs | 20 +-- tools/cli/src/cli/lib.rs | 4 +- tools/cli/src/cli/oauth2.rs | 38 ++--- tools/cli/src/cli/person.rs | 30 ++-- tools/cli/src/cli/recycle.rs | 6 +- tools/cli/src/cli/serviceaccount.rs | 34 ++--- tools/cli/src/cli/session.rs | 72 +++++---- tools/cli/src/cli/synch.rs | 20 +-- tools/cli/src/cli/system_config/badlist.rs | 6 +- .../cli/src/cli/system_config/denied_names.rs | 6 +- tools/cli/src/opt/kanidm.rs | 33 +++- 27 files changed, 537 insertions(+), 202 deletions(-) diff --git a/.github/workflows/rust_build.yml b/.github/workflows/rust_build.yml index e8998ca0b..391aa18db 100644 --- a/.github/workflows/rust_build.yml +++ b/.github/workflows/rust_build.yml @@ -64,6 +64,7 @@ jobs: RUSTC_WRAPPER: sccache CARGO_INCREMENTAL: 0 CARGO_TERM_COLOR: always + MALLOC_CONF: "thp:always,metadata_thp:always" steps: - uses: actions/checkout@v4 - name: Install Rust diff --git a/Cargo.lock b/Cargo.lock index 16b74f492..8f96b24a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "assert_cmd" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-compression" version = "0.4.3" @@ -891,7 +906,7 @@ dependencies = [ "clap", "criterion-plot", "is-terminal", - "itertools", + "itertools 0.10.5", "num-traits", "once_cell", "oorandom", @@ -912,7 +927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools", + "itertools 0.10.5", ] [[package]] @@ -1265,6 +1280,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.8.1" @@ -1316,6 +1337,12 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dunce" version = "1.0.4" @@ -1457,6 +1484,18 @@ dependencies = [ "libc", ] +[[package]] +name = "escargot" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768064bd3a0e2bedcba91dc87ace90beea91acc41b6a01a3ca8e9aa8827461bf" +dependencies = [ + "log", + "once_cell", + "serde", + "serde_json", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -2819,6 +2858,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -3179,7 +3227,9 @@ dependencies = [ name = "kanidmd_testkit" version = "1.1.0-rc.14-dev" dependencies = [ + "assert_cmd", "compact_jwt", + "escargot", "fantoccini", "futures", "http", @@ -3199,6 +3249,7 @@ dependencies = [ "serde", "serde_json", "sketching", + "tempfile", "testkit-macros", "time", "tokio", @@ -4295,6 +4346,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" +dependencies = [ + "anstyle", + "difflib", + "itertools 0.11.0", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" version = "0.1.25" @@ -5342,6 +5421,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "testkit-macros" version = "0.1.0" @@ -5902,6 +5987,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.4.0" @@ -6598,7 +6692,7 @@ checksum = "103fa851fff70ea29af380e87c25c48ff7faac5c530c70bd0e65366d4e0c94e4" dependencies = [ "derive_builder", "fancy-regex", - "itertools", + "itertools 0.10.5", "js-sys", "lazy_static", "quick-error", diff --git a/book/src/integrations/pam_and_nsswitch.md b/book/src/integrations/pam_and_nsswitch.md index c02281175..96bb4e2d2 100644 --- a/book/src/integrations/pam_and_nsswitch.md +++ b/book/src/integrations/pam_and_nsswitch.md @@ -183,6 +183,8 @@ cp -a /etc/pam.d /root/pam.d.backup Documentation examples for the following Linux distributions are available: -* [Fedora](pam_and_nsswitch/fedora.md) -* [SUSE / OpenSUSE](pam_and_nsswitch/suse.md) -* Debian / Ubuntu - when one generates packages [from the repository tools](https://github.com/kanidm/kanidm/tree/master/platform/debian), configuration is modified on install. +- [SUSE / OpenSUSE](pam_and_nsswitch/suse.md) +- [Fedora](pam_and_nsswitch/fedora.md) +- Debian / Ubuntu - when one generates packages + [from the repository tools](https://github.com/kanidm/kanidm/tree/master/platform/debian), + configuration is modified on install. diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 08c138efb..b133aef42 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -25,7 +25,9 @@ use std::path::Path; use std::time::Duration; use kanidm_proto::constants::uri::V1_AUTH_VALID; -use kanidm_proto::constants::{APPLICATION_JSON, ATTR_NAME, KOPID, KSESSIONID, KVERSION}; +use kanidm_proto::constants::{ + APPLICATION_JSON, ATTR_NAME, CLIENT_TOKEN_CACHE, KOPID, KSESSIONID, KVERSION, +}; use kanidm_proto::v1::*; use reqwest::header::CONTENT_TYPE; use reqwest::Response; @@ -85,6 +87,8 @@ pub struct KanidmClientBuilder { ca: Option, connect_timeout: Option, use_system_proxies: bool, + /// Where to store auth tokens, only use in testing! + token_cache_path: Option, } impl Display for KanidmClientBuilder { @@ -103,7 +107,14 @@ impl Display for KanidmClientBuilder { Some(value) => writeln!(f, "connect_timeout: {}", value)?, None => writeln!(f, "connect_timeout: unset")?, } - writeln!(f, "use_system_proxies: {}", self.use_system_proxies) + writeln!(f, "use_system_proxies: {}", self.use_system_proxies)?; + writeln!( + f, + "token_cache_path: {}", + self.token_cache_path + .clone() + .unwrap_or(CLIENT_TOKEN_CACHE.to_string()) + ) } } @@ -120,6 +131,7 @@ fn test_kanidmclientbuilder_display() { ca: None, connect_timeout: Some(420), use_system_proxies: true, + token_cache_path: Some(CLIENT_TOKEN_CACHE.to_string()), }; println!("foo {}", testclient); assert!(testclient.to_string().contains("verify_ca: true")); @@ -141,6 +153,8 @@ pub struct KanidmClient { pub(crate) bearer_token: RwLock>, pub(crate) auth_session_id: RwLock>, pub(crate) check_version: Mutex, + /// Where to store the tokens when you auth, only modify in testing. + token_cache_path: String, } #[cfg(target_family = "unix")] @@ -163,6 +177,7 @@ impl KanidmClientBuilder { ca: None, connect_timeout: None, use_system_proxies: true, + token_cache_path: None, } } @@ -218,6 +233,7 @@ impl KanidmClientBuilder { ca, connect_timeout, use_system_proxies, + token_cache_path, } = self; // Process and apply all our options if they exist. let address = match kcc.uri { @@ -241,6 +257,7 @@ impl KanidmClientBuilder { ca, connect_timeout, use_system_proxies, + token_cache_path, }) } @@ -313,57 +330,37 @@ impl KanidmClientBuilder { pub fn address(self, address: String) -> Self { KanidmClientBuilder { address: Some(address), - verify_ca: self.verify_ca, - verify_hostnames: self.verify_hostnames, - ca: self.ca, - connect_timeout: self.connect_timeout, - use_system_proxies: self.use_system_proxies, + ..self } } pub fn danger_accept_invalid_hostnames(self, accept_invalid_hostnames: bool) -> Self { KanidmClientBuilder { - address: self.address, - verify_ca: self.verify_ca, // We have to flip the bool state here due to english language. verify_hostnames: !accept_invalid_hostnames, - ca: self.ca, - connect_timeout: self.connect_timeout, - use_system_proxies: self.use_system_proxies, + ..self } } pub fn danger_accept_invalid_certs(self, accept_invalid_certs: bool) -> Self { KanidmClientBuilder { - address: self.address, // We have to flip the bool state here due to english language. verify_ca: !accept_invalid_certs, - verify_hostnames: self.verify_hostnames, - ca: self.ca, - connect_timeout: self.connect_timeout, - use_system_proxies: self.use_system_proxies, + ..self } } pub fn connect_timeout(self, secs: u64) -> Self { KanidmClientBuilder { - address: self.address, - verify_ca: self.verify_ca, - verify_hostnames: self.verify_hostnames, - ca: self.ca, connect_timeout: Some(secs), - use_system_proxies: self.use_system_proxies, + ..self } } pub fn no_proxy(self) -> Self { KanidmClientBuilder { - address: self.address, - verify_ca: self.verify_ca, - verify_hostnames: self.verify_hostnames, - ca: self.ca, - connect_timeout: self.connect_timeout, use_system_proxies: false, + ..self } } @@ -376,12 +373,8 @@ impl KanidmClientBuilder { })?; Ok(KanidmClientBuilder { - address: self.address, - verify_ca: self.verify_ca, - verify_hostnames: self.verify_hostnames, ca: Some(ca), - connect_timeout: self.connect_timeout, - use_system_proxies: self.use_system_proxies, + ..self }) } @@ -396,7 +389,6 @@ impl KanidmClientBuilder { "verify_hostnames set to false in client configuration - this may allow network interception of passwords!" ); } - if !address.starts_with("https://") { warn!("Address does not start with 'https://' - this may allow network interception of passwords!"); } @@ -463,6 +455,11 @@ impl KanidmClientBuilder { let origin = Url::parse(&uri.origin().ascii_serialization()).expect("failed to parse origin"); + let token_cache_path = match self.token_cache_path.clone() { + Some(val) => val.to_string(), + None => CLIENT_TOKEN_CACHE.to_string(), + }; + Ok(KanidmClient { client, addr: address, @@ -471,6 +468,7 @@ impl KanidmClientBuilder { origin, auth_session_id: RwLock::new(None), check_version: Mutex::new(true), + token_cache_path, }) } } @@ -561,6 +559,10 @@ impl KanidmClient { Ok(()) } + pub fn get_token_cache_path(&self) -> String { + self.token_cache_path.clone() + } + /// Check that we're getting the right version back from the server. async fn expect_version(&self, response: &reqwest::Response) { let mut guard = self.check_version.lock().await; diff --git a/libs/sketching/src/lib.rs b/libs/sketching/src/lib.rs index e405e383d..0058c630f 100644 --- a/libs/sketching/src/lib.rs +++ b/libs/sketching/src/lib.rs @@ -2,22 +2,33 @@ #![warn(unused_extern_crates)] #![allow(non_snake_case)] use num_enum::{IntoPrimitive, TryFromPrimitive}; +use tracing_forest::printer::TestCapturePrinter; +use tracing_forest::tag::NoTag; use tracing_forest::util::*; use tracing_forest::Tag; +use tracing_subscriber::prelude::*; pub mod macros; pub use {tracing, tracing_forest, tracing_subscriber}; +/// Start up the logging for test mode. pub fn test_init() { - // tracing_subscriber::fmt::try_init() - let _ = tracing_forest::test_init(); - /* - let _ = Registry::default().with(ForestLayer::new( - TestCapturePrinter::new(), - NoTag, - )).try_init(); - */ + let filter = EnvFilter::from_default_env() + .add_directive(LevelFilter::TRACE.into()) + // escargot builds cargo packages while we integration test and is SUPER noisy. + .add_directive( + "escargot=ERROR" + .parse() + .expect("failed to generate log filter"), + ) + // hyper's very noisy in debug mode with connectivity-related things that we only need in extreme cases. + .add_directive("hyper=INFO".parse().expect("failed to generate log filter")); + + // start the logging! + let _ = tracing_subscriber::Registry::default() + .with(ForestLayer::new(TestCapturePrinter::new(), NoTag).with_filter(filter)) + .try_init(); } /// This is for tagging events. Currently not wired in. diff --git a/proto/src/constants.rs b/proto/src/constants.rs index 21126b145..15151f64c 100644 --- a/proto/src/constants.rs +++ b/proto/src/constants.rs @@ -2,6 +2,9 @@ //! pub mod uri; +/// The default location for the `kanidm` CLI tool's token cache. +pub const CLIENT_TOKEN_CACHE: &str = "~/.cache/kanidm_tokens"; + pub const CONTENT_TYPE_JPG: &str = "image/jpeg"; pub const CONTENT_TYPE_PNG: &str = "image/png"; pub const CONTENT_TYPE_GIF: &str = "image/gif"; diff --git a/scripts/oauth_proxy/README.md b/scripts/oauth_proxy/README.md index 0036743ba..03ab7e8b4 100644 --- a/scripts/oauth_proxy/README.md +++ b/scripts/oauth_proxy/README.md @@ -5,5 +5,6 @@ This dir has some things for setting up a simple OAuth2 RS so things can get tes ## Quick Setup 1. Run the `setup_dev_environment.sh` script and set a credential for `testuser`. -2. Look for the OAuth2 Secret in the script output and copy it into a file called `client.secret` in this dir. +2. Look for the OAuth2 Secret in the script output and copy it into a file called `client.secret` in + this dir. 3. Run `./run_proxy.sh` to start the proxy, and then go to the URL and do the thing! diff --git a/server/daemon/src/opt.rs b/server/daemon/src/opt.rs index 30bd5cd76..8d9e8b2e2 100644 --- a/server/daemon/src/opt.rs +++ b/server/daemon/src/opt.rs @@ -4,7 +4,7 @@ struct CommonOpt { #[clap(short, long = "config", env = "KANIDM_CONFIG")] config_path: Option, /// Log format (still in very early development) - #[clap(short, long = "output", env = "KANIDM_OUTPUT", default_value="text")] + #[clap(short, long = "output", env = "KANIDM_OUTPUT", default_value = "text")] output_mode: String, } @@ -60,14 +60,13 @@ struct DbScanListIndex { commonopts: CommonOpt, } - -#[derive(Debug,Parser)] +#[derive(Debug, Parser)] struct HealthCheckArgs { /// Disable TLS verification #[clap(short, long, action)] verify_tls: bool, /// Check the 'origin' URL from the server configuration file, instead of the 'address' - #[clap(short='O', long, action)] + #[clap(short = 'O', long, action)] check_origin: bool, #[clap(flatten)] commonopts: CommonOpt, @@ -126,7 +125,7 @@ enum DbScanOpt { } #[derive(Debug, Parser)] -#[command(name="kanidmd")] +#[command(name = "kanidmd")] struct KanidmdParser { #[command(subcommand)] commands: KanidmdOpt, @@ -199,6 +198,6 @@ enum KanidmdOpt { HealthCheck(HealthCheckArgs), /// Print the program version and exit - #[clap(name="version")] - Version(CommonOpt) + #[clap(name = "version")] + Version(CommonOpt), } diff --git a/server/testkit-macros/src/lib.rs b/server/testkit-macros/src/lib.rs index 42af50275..6ca72af02 100644 --- a/server/testkit-macros/src/lib.rs +++ b/server/testkit-macros/src/lib.rs @@ -16,8 +16,39 @@ mod entry; extern crate proc_macro; use proc_macro::TokenStream; +use quote::quote; #[proc_macro_attribute] pub fn test(args: TokenStream, item: TokenStream) -> TokenStream { entry::test(args, item) } + +#[proc_macro] +/// used in testkit to build and run the kanidm binary with the correct environment variables +pub fn cli_kanidm(_input: TokenStream) -> TokenStream { + let code = quote! { + { + // get the manifest path for the kanidm binary + let cli_manifest_file_path = + format!("{}/../../tools/cli/Cargo.toml", env!("CARGO_MANIFEST_DIR")); + let cli_manifest_file = std::path::Path::new(&cli_manifest_file_path) + .canonicalize() + .unwrap(); + + // make sure we're building/running the current version + let mut kanidm = escargot::CargoBuild::new() + .bin("kanidm") + .current_release() + .current_target() + .manifest_path(&cli_manifest_file) + .run() + .unwrap(); + let mut kanidm = kanidm.command(); + kanidm.env("KANIDM_URL", &rsclient.get_url().to_string()); + kanidm.env("KANIDM_TOKEN_CACHE_PATH", &token_cache_path); + kanidm + } + }; + + code.into() +} diff --git a/server/testkit/Cargo.toml b/server/testkit/Cargo.toml index 287e122d0..6d9cce7f2 100644 --- a/server/testkit/Cargo.toml +++ b/server/testkit/Cargo.toml @@ -20,36 +20,44 @@ doctest = false [features] default = [] # Enables webdriver tests, you need to be running a webdriver server -webdriver = ["fantoccini"] +webdriver = [] [dependencies] +assert_cmd = "2.0.12" +escargot = "0.5.8" +hyper-tls = { workspace = true } +http = { workspace = true } kanidm_client = { workspace = true } kanidm_proto = { workspace = true } kanidmd_core = { workspace = true } kanidmd_lib = { workspace = true } -# used for webdriver testing -hyper-tls = { workspace = true } -http = { workspace = true } -# used for webdriver testing -fantoccini = { version = "0.19.3", optional = true } -serde = { workspace = true } -url = { workspace = true, features = ["serde"] } -reqwest = { workspace = true, default-features = false, features = ["cookies"] } -sketching = { workspace = true } -testkit-macros = { workspace = true } -tracing = { workspace = true, features = ["attributes"] } -tokio = { workspace = true, features = ["net", "sync", "io-util", "macros"] } -openssl = { workspace = true } lazy_static = { workspace = true } +openssl = { workspace = true } petgraph = { version = "0.6.4", features = ["serde", "serde-1"] } -regex.workspace = true - +regex = { workspace = true } +reqwest = { workspace = true, default-features = false, features = ["cookies"] } +serde = { workspace = true } +sketching = { workspace = true } +tempfile = { workspace = true } +testkit-macros = { workspace = true } +tokio = { workspace = true, features = [ + "net", + "sync", + "io-util", + "macros", + "rt", +] } +tracing = { workspace = true, features = ["attributes"] } +url = { workspace = true, features = ["serde"] } [build-dependencies] kanidm_build_profiles = { workspace = true } [dev-dependencies] compact_jwt = { workspace = true } +# used for webdriver testing +fantoccini = { version = "0.19.3" } + serde_json = { workspace = true } webauthn-authenticator-rs = { workspace = true } oauth2_ext = { workspace = true, default-features = false } diff --git a/server/testkit/src/lib.rs b/server/testkit/src/lib.rs index e51d22462..0e8c734a6 100644 --- a/server/testkit/src/lib.rs +++ b/server/testkit/src/lib.rs @@ -26,6 +26,8 @@ use tokio::task; pub const ADMIN_TEST_USER: &str = "admin"; pub const ADMIN_TEST_PASSWORD: &str = "integration test admin password"; +pub const IDM_ADMIN_TEST_USER: &str = "idm_admin"; +pub const IDM_ADMIN_TEST_PASSWORD: &str = "integration idm admin password"; pub const NOT_ADMIN_TEST_USERNAME: &str = "krab_test_user"; pub const NOT_ADMIN_TEST_PASSWORD: &str = "eicieY7ahchaoCh0eeTa"; diff --git a/server/testkit/tests/integration.rs b/server/testkit/tests/integration.rs index 3d83e5c30..3904c3219 100644 --- a/server/testkit/tests/integration.rs +++ b/server/testkit/tests/integration.rs @@ -1,7 +1,15 @@ //! Integration tests using browser automation +use std::process::Output; + +use tempfile::tempdir; + use kanidm_client::KanidmClient; -use kanidmd_testkit::login_put_admin_idm_admins; +use kanidmd_testkit::{ + login_put_admin_idm_admins, ADMIN_TEST_PASSWORD, IDM_ADMIN_TEST_PASSWORD, IDM_ADMIN_TEST_USER, + NOT_ADMIN_TEST_USERNAME, +}; +use testkit_macros::cli_kanidm; /// Tries to handle closing the webdriver session if there's an error #[allow(unused_macros)] @@ -19,7 +27,7 @@ macro_rules! handle_error { /// Tries to get the webdriver client, trying the default chromedriver port if the default selenium port doesn't work #[allow(dead_code)] -#[cfg(feature = "webdriver")] +#[cfg(all(feature = "webdriver", any(test, debug_assertions)))] async fn get_webdriver_client() -> fantoccini::Client { use fantoccini::wd::Capabilities; use serde_json::json; @@ -219,3 +227,135 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) { .await .is_err()); } + +/// run a test command as the admin user +fn test_cmd_admin(token_cache_path: &str, rsclient: &KanidmClient, cmd: &str) -> Output { + let split_cmd: Vec<&str> = cmd.split_ascii_whitespace().collect(); + test_cmd_admin_split(token_cache_path, rsclient, &split_cmd) +} +/// run a test command as the admin user +fn test_cmd_admin_split(token_cache_path: &str, rsclient: &KanidmClient, cmd: &[&str]) -> Output { + println!( + "##################################\nrunning {}\n##################################", + cmd.join(" ") + ); + let res = cli_kanidm!() + .env("KANIDM_PASSWORD", ADMIN_TEST_PASSWORD) + .args(cmd) + .output() + .unwrap(); + println!("############ result ##################"); + println!("status: {:?}", res.status); + println!("stdout: {}", String::from_utf8_lossy(&res.stdout)); + println!("stderr: {}", String::from_utf8_lossy(&res.stderr)); + println!("######################################"); + assert!(res.status.success()); + res +} + +/// run a test command as the idm_admin user +fn test_cmd_idm_admin(token_cache_path: &str, rsclient: &KanidmClient, cmd: &str) -> Output { + println!("##############################\nrunning {}", cmd); + let res = cli_kanidm!() + .env("KANIDM_PASSWORD", IDM_ADMIN_TEST_PASSWORD) + .args(cmd.split(" ")) + .output() + .unwrap(); + println!("##############################\n{} result: {:?}", cmd, res); + assert!(res.status.success()); + res +} + +#[kanidmd_testkit::test] +/// Testing the CLI doing things. +async fn test_integration_with_assert_cmd(rsclient: KanidmClient) { + // setup the admin things + login_put_admin_idm_admins(&rsclient).await; + + rsclient + .idm_person_account_primary_credential_set_password( + IDM_ADMIN_TEST_USER, + IDM_ADMIN_TEST_PASSWORD, + ) + .await + .expect(&format!("Failed to set {} password", IDM_ADMIN_TEST_USER)); + + let token_cache_dir = tempdir().unwrap(); + let token_cache_path = format!("{}/kanidm_tokens", token_cache_dir.path().display()); + + // we have to spawn in another thread for ... reasons + assert!(tokio::task::spawn_blocking(move || { + let anon_login = cli_kanidm!() + .args(&["login", "-D", "anonymous"]) + .output() + .unwrap(); + println!("Login Output: {:?}", anon_login); + + let anon_whoami = cli_kanidm!() + .args(&["self", "whoami", "-D", "anonymous"]) + .output() + .unwrap(); + assert!(anon_whoami.status.success()); + println!("Output: {:?}", anon_whoami); + + test_cmd_admin(&token_cache_path, &rsclient, "login -D admin"); + + // login as idm_admin + test_cmd_idm_admin(&token_cache_path, &rsclient, "login -D idm_admin"); + test_cmd_admin_split( + &token_cache_path, + &rsclient, + &[ + "service-account", + "create", + NOT_ADMIN_TEST_USERNAME, + "Test account", + "-D", + "admin", + "-o", + "json", + ], + ); + + test_cmd_admin( + &token_cache_path, + &rsclient, + &format!("service-account get -D admin {}", NOT_ADMIN_TEST_USERNAME), + ); + // updating the display name + test_cmd_admin( + &token_cache_path, + &rsclient, + &format!( + "service-account update -D admin {} --displayname cheeseballs", + NOT_ADMIN_TEST_USERNAME + ), + ); + // updating the email + test_cmd_admin( + &token_cache_path, + &rsclient, + &format!( + "service-account update -D admin {} --mail foo@bar.com", + NOT_ADMIN_TEST_USERNAME + ), + ); + + // checking the email was changed + let sad = test_cmd_admin( + &token_cache_path, + &rsclient, + &format!( + "service-account get -D admin -o json {}", + NOT_ADMIN_TEST_USERNAME + ), + ); + let str_output: String = String::from_utf8_lossy(&sad.stdout).into(); + assert!(str_output.contains("foo@bar.com")); + + true + }) + .await + .unwrap()); + println!("Success!"); +} diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index f34e153a2..87fc4fa52 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -70,6 +70,8 @@ features = ["crossterm-backend"] clap = { workspace = true, features = ["derive"] } clap_complete = { workspace = true } kanidm_build_profiles = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } uuid = { workspace = true } url = { workspace = true } diff --git a/tools/cli/src/cli/common.rs b/tools/cli/src/cli/common.rs index b635ea0c3..d757a357f 100644 --- a/tools/cli/src/cli/common.rs +++ b/tools/cli/src/cli/common.rs @@ -100,7 +100,7 @@ impl CommonOpt { async fn try_to_client(&self, optype: OpType) -> Result { let client = self.to_unauth_client(); // Read the token file. - let tokens = match read_tokens() { + let tokens = match read_tokens(&client.get_token_cache_path()) { Ok(t) => t, Err(_e) => { error!("Error retrieving authentication token store"); @@ -187,7 +187,7 @@ impl CommonOpt { } else { // Unable to automatically select the user because multiple tokens exist // so we'll prompt the user to select one - match prompt_for_username_get_values() { + match prompt_for_username_get_values(&client.get_token_cache_path()) { Ok(tuple) => tuple, Err(msg) => { error!("Error: {}", msg); @@ -309,8 +309,8 @@ impl CommonOpt { /// This parses the token store and prompts the user to select their username, returns the username/token as a tuple of Strings /// /// Used to reduce duplication in implementing [prompt_for_username_get_username] and [prompt_for_username_get_token] -pub fn prompt_for_username_get_values() -> Result<(String, String), String> { - let tokens = match read_tokens() { +pub fn prompt_for_username_get_values(token_cache_path: &str) -> Result<(String, String), String> { + let tokens = match read_tokens(token_cache_path) { Ok(value) => value, _ => return Err("Error retrieving authentication token store".to_string()), }; @@ -353,8 +353,8 @@ pub fn prompt_for_username_get_values() -> Result<(String, String), String> { /// This parses the token store and prompts the user to select their username, returns the username as a String /// /// Powered by [prompt_for_username_get_values] -pub fn prompt_for_username_get_username() -> Result { - match prompt_for_username_get_values() { +pub fn prompt_for_username_get_username(token_cache_path: &str) -> Result { + match prompt_for_username_get_values(token_cache_path) { Ok(value) => { let (f_user, _) = value; Ok(f_user) diff --git a/tools/cli/src/cli/domain.rs b/tools/cli/src/cli/domain.rs index 691eee55f..a2bec7821 100644 --- a/tools/cli/src/cli/domain.rs +++ b/tools/cli/src/cli/domain.rs @@ -24,7 +24,7 @@ impl DomainOpt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &opt.copt.output_mode), + Err(e) => handle_client_error(e, opt.copt.output_mode), } } DomainOpt::SetLdapBasedn { copt, new_basedn } => { @@ -35,28 +35,28 @@ impl DomainOpt { let client = copt.to_client(OpType::Write).await; match client.idm_domain_set_ldap_basedn(new_basedn).await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } DomainOpt::SetLdapAllowUnixPasswordBind { copt, enable } => { let client = copt.to_client(OpType::Write).await; match client.idm_set_ldap_allow_unix_password_bind(*enable).await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } DomainOpt::Show(copt) => { let client = copt.to_client(OpType::Read).await; match client.idm_domain_get().await { Ok(e) => println!("{}", e), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } DomainOpt::ResetTokenKey(copt) => { let client = copt.to_client(OpType::Write).await; match client.idm_domain_reset_token_key().await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } } diff --git a/tools/cli/src/cli/group/account_policy.rs b/tools/cli/src/cli/group/account_policy.rs index 65926686c..67b895cd9 100644 --- a/tools/cli/src/cli/group/account_policy.rs +++ b/tools/cli/src/cli/group/account_policy.rs @@ -15,7 +15,7 @@ impl GroupAccountPolicyOpt { GroupAccountPolicyOpt::Enable { name, copt } => { let client = copt.to_client(OpType::Write).await; if let Err(e) = client.group_account_policy_enable(name).await { - handle_client_error(e, &copt.output_mode); + handle_client_error(e, copt.output_mode); } else { println!("Group enabled for account policy."); } @@ -26,7 +26,7 @@ impl GroupAccountPolicyOpt { .group_account_policy_authsession_expiry_set(name, *expiry) .await { - handle_client_error(e, &copt.output_mode); + handle_client_error(e, copt.output_mode); } else { println!("Updated authsession expiry."); } @@ -37,7 +37,7 @@ impl GroupAccountPolicyOpt { .group_account_policy_privilege_expiry_set(name, *expiry) .await { - handle_client_error(e, &copt.output_mode); + handle_client_error(e, copt.output_mode); } else { println!("Updated authsession expiry."); } diff --git a/tools/cli/src/cli/group/mod.rs b/tools/cli/src/cli/group/mod.rs index 26e7c60f0..595ff56dd 100644 --- a/tools/cli/src/cli/group/mod.rs +++ b/tools/cli/src/cli/group/mod.rs @@ -38,7 +38,7 @@ impl GroupOpt { } OutputMode::Text => r.iter().for_each(|ent| println!("{}", ent)), }, - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } GroupOpt::Get(gcopt) => { @@ -55,7 +55,7 @@ impl GroupOpt { OutputMode::Text => println!("{}", e), }, Ok(None) => warn!("No matching group '{}'", gcopt.name.as_str()), - Err(e) => handle_client_error(e, &gcopt.copt.output_mode), + Err(e) => handle_client_error(e, gcopt.copt.output_mode), } } GroupOpt::Create(gcopt) => { @@ -70,14 +70,14 @@ impl GroupOpt { GroupOpt::Delete(gcopt) => { let client = gcopt.copt.to_client(OpType::Write).await; match client.idm_group_delete(gcopt.name.as_str()).await { - Err(e) => handle_client_error(e, &gcopt.copt.output_mode), + Err(e) => handle_client_error(e, gcopt.copt.output_mode), Ok(_) => println!("Successfully deleted group {}", gcopt.name.as_str()), } } GroupOpt::PurgeMembers(gcopt) => { let client = gcopt.copt.to_client(OpType::Write).await; match client.idm_group_purge_members(gcopt.name.as_str()).await { - Err(e) => handle_client_error(e, &gcopt.copt.output_mode), + Err(e) => handle_client_error(e, gcopt.copt.output_mode), Ok(_) => println!( "Successfully purged members of group {}", gcopt.name.as_str() @@ -89,7 +89,7 @@ impl GroupOpt { match client.idm_group_get_members(gcopt.name.as_str()).await { Ok(Some(groups)) => groups.iter().for_each(|m| println!("{:?}", m)), Ok(None) => warn!("No members in group {}", gcopt.name.as_str()), - Err(e) => handle_client_error(e, &gcopt.copt.output_mode), + Err(e) => handle_client_error(e, gcopt.copt.output_mode), } } GroupOpt::AddMembers(gcopt) => { @@ -100,7 +100,7 @@ impl GroupOpt { .idm_group_add_members(gcopt.name.as_str(), &new_members) .await { - Err(e) => handle_client_error(e, &gcopt.copt.output_mode), + Err(e) => handle_client_error(e, gcopt.copt.output_mode), Ok(_) => println!( "Successfully added {:?} to group \"{}\"", &new_members, @@ -119,7 +119,7 @@ impl GroupOpt { { Err(e) => { error!("Failed to remove members!"); - handle_client_error(e, &gcopt.copt.output_mode) + handle_client_error(e, gcopt.copt.output_mode) } Ok(_) => println!("Successfully removed members from {}", gcopt.name.as_str()), } @@ -133,7 +133,7 @@ impl GroupOpt { .idm_group_set_members(gcopt.name.as_str(), &new_members) .await { - Err(e) => handle_client_error(e, &gcopt.copt.output_mode), + Err(e) => handle_client_error(e, gcopt.copt.output_mode), Ok(_) => println!("Successfully set members for group {}", gcopt.name.as_str()), } } @@ -142,7 +142,7 @@ impl GroupOpt { let client = gcopt.copt.to_client(OpType::Read).await; match client.idm_group_unix_token_get(gcopt.name.as_str()).await { Ok(token) => println!("{}", token), - Err(e) => handle_client_error(e, &gcopt.copt.output_mode), + Err(e) => handle_client_error(e, gcopt.copt.output_mode), } } GroupPosix::Set(gcopt) => { @@ -151,7 +151,7 @@ impl GroupOpt { .idm_group_unix_extend(gcopt.name.as_str(), gcopt.gidnumber) .await { - Err(e) => handle_client_error(e, &gcopt.copt.output_mode), + Err(e) => handle_client_error(e, gcopt.copt.output_mode), Ok(_) => println!( "Success adding POSIX configuration for group {}", gcopt.name.as_str() diff --git a/tools/cli/src/cli/lib.rs b/tools/cli/src/cli/lib.rs index b0e6a2cf9..1b5c70556 100644 --- a/tools/cli/src/cli/lib.rs +++ b/tools/cli/src/cli/lib.rs @@ -43,7 +43,7 @@ mod system_config; mod webauthn; /// Throws an error and exits the program when we get an error -pub(crate) fn handle_client_error(response: ClientError, _output_mode: &OutputMode) { +pub(crate) fn handle_client_error(response: ClientError, _output_mode: OutputMode) { match response { ClientError::Http(status, error, message) => { let error_msg = match error { @@ -101,7 +101,7 @@ impl SelfOpt { } } } - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } SelfOpt::IdentifyUser(copt) => { diff --git a/tools/cli/src/cli/oauth2.rs b/tools/cli/src/cli/oauth2.rs index 0b6f470b4..8de186875 100644 --- a/tools/cli/src/cli/oauth2.rs +++ b/tools/cli/src/cli/oauth2.rs @@ -46,7 +46,7 @@ impl Oauth2Opt { } OutputMode::Text => r.iter().for_each(|ent| println!("{}", ent)), }, - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } Oauth2Opt::Get(nopt) => { @@ -54,7 +54,7 @@ impl Oauth2Opt { match client.idm_oauth2_rs_get(nopt.name.as_str()).await { Ok(Some(e)) => println!("{}", e), Ok(None) => println!("No matching entries"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::CreateBasic { @@ -73,7 +73,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } Oauth2Opt::CreatePublic { @@ -92,7 +92,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } Oauth2Opt::UpdateScopeMap(cbopt) => { @@ -106,7 +106,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &cbopt.nopt.copt.output_mode), + Err(e) => handle_client_error(e, cbopt.nopt.copt.output_mode), } } Oauth2Opt::DeleteScopeMap(cbopt) => { @@ -116,7 +116,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &cbopt.nopt.copt.output_mode), + Err(e) => handle_client_error(e, cbopt.nopt.copt.output_mode), } } Oauth2Opt::UpdateSupScopeMap(cbopt) => { @@ -146,7 +146,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &cbopt.nopt.copt.output_mode), + Err(e) => handle_client_error(e, cbopt.nopt.copt.output_mode), } } Oauth2Opt::ResetSecrets(cbopt) => { @@ -165,7 +165,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &cbopt.copt.output_mode), + Err(e) => handle_client_error(e, cbopt.copt.output_mode), } } Oauth2Opt::ShowBasicSecret(nopt) => { @@ -184,14 +184,14 @@ impl Oauth2Opt { Ok(None) => { eprintln!("No secret configured"); } - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::Delete(nopt) => { let client = nopt.copt.to_client(OpType::Write).await; match client.idm_oauth2_rs_delete(nopt.name.as_str()).await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::SetDisplayname(cbopt) => { @@ -210,7 +210,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &cbopt.nopt.copt.output_mode), + Err(e) => handle_client_error(e, cbopt.nopt.copt.output_mode), } } Oauth2Opt::SetName { nopt, name } => { @@ -229,7 +229,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::SetLandingUrl { nopt, url } => { @@ -248,21 +248,21 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::EnablePkce(nopt) => { let client = nopt.copt.to_client(OpType::Write).await; match client.idm_oauth2_rs_enable_pkce(nopt.name.as_str()).await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::DisablePkce(nopt) => { let client = nopt.copt.to_client(OpType::Write).await; match client.idm_oauth2_rs_disable_pkce(nopt.name.as_str()).await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::EnableLegacyCrypto(nopt) => { @@ -272,7 +272,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::DisableLegacyCrypto(nopt) => { @@ -282,7 +282,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::PreferShortUsername(nopt) => { @@ -292,7 +292,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } Oauth2Opt::PreferSPNUsername(nopt) => { @@ -302,7 +302,7 @@ impl Oauth2Opt { .await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } } diff --git a/tools/cli/src/cli/person.rs b/tools/cli/src/cli/person.rs index 3436719fb..f25962da8 100644 --- a/tools/cli/src/cli/person.rs +++ b/tools/cli/src/cli/person.rs @@ -83,7 +83,7 @@ impl PersonOpt { "No RADIUS secret set for user {}", aopt.aopts.account_id.as_str(), ), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } AccountRadius::Generate(aopt) => { @@ -133,7 +133,7 @@ impl PersonOpt { .await { Ok(token) => println!("{}", token), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } PersonPosix::Set(aopt) => { @@ -146,7 +146,7 @@ impl PersonOpt { ) .await { - handle_client_error(e, &aopt.copt.output_mode) + handle_client_error(e, aopt.copt.output_mode) } } PersonPosix::SetPassword(aopt) => { @@ -166,7 +166,7 @@ impl PersonOpt { ) .await { - handle_client_error(e, &aopt.copt.output_mode) + handle_client_error(e, aopt.copt.output_mode) } } }, // end PersonOpt::Posix @@ -186,7 +186,7 @@ impl PersonOpt { } } } - Err(e) => handle_client_error(e, &apo.copt.output_mode), + Err(e) => handle_client_error(e, apo.copt.output_mode), } } AccountUserAuthToken::Destroy { @@ -204,7 +204,7 @@ impl PersonOpt { } Err(e) => { error!("Error destroying account session"); - handle_client_error(e, &copt.output_mode); + handle_client_error(e, copt.output_mode); } } } @@ -218,7 +218,7 @@ impl PersonOpt { .await { Ok(pkeys) => pkeys.iter().for_each(|pkey| println!("{}", pkey)), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } AccountSsh::Add(aopt) => { @@ -231,7 +231,7 @@ impl PersonOpt { ) .await { - handle_client_error(e, &aopt.copt.output_mode) + handle_client_error(e, aopt.copt.output_mode) } } AccountSsh::Delete(aopt) => { @@ -243,7 +243,7 @@ impl PersonOpt { ) .await { - handle_client_error(e, &aopt.copt.output_mode) + handle_client_error(e, aopt.copt.output_mode) } } }, // end PersonOpt::Ssh @@ -260,7 +260,7 @@ impl PersonOpt { } OutputMode::Text => r.iter().for_each(|ent| println!("{}", ent)), }, - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } PersonOpt::Update(aopt) => { @@ -276,7 +276,7 @@ impl PersonOpt { .await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } PersonOpt::Get(aopt) => { @@ -295,7 +295,7 @@ impl PersonOpt { OutputMode::Text => println!("{}", e), }, Ok(None) => println!("No matching entries"), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } PersonOpt::Delete(aopt) => { @@ -321,7 +321,7 @@ impl PersonOpt { modmessage.status = MessageStatus::Failure; eprintln!("{}", modmessage); - // handle_client_error(e, &aopt.copt.output_mode), + // handle_client_error(e, aopt.copt.output_mode), } Ok(result) => { debug!("{:?}", result); @@ -345,7 +345,7 @@ impl PersonOpt { acopt.aopts.account_id.as_str(), ) } - Err(e) => handle_client_error(e, &acopt.copt.output_mode), + Err(e) => handle_client_error(e, acopt.copt.output_mode), } } PersonOpt::Validity { commands } => match commands { @@ -434,7 +434,7 @@ impl PersonOpt { } }; match res { - Err(e) => handle_client_error(e, &ano.copt.output_mode), + Err(e) => handle_client_error(e, ano.copt.output_mode), _ => println!("Success"), }; } diff --git a/tools/cli/src/cli/recycle.rs b/tools/cli/src/cli/recycle.rs index f2e4d50a9..90e7cc1e5 100644 --- a/tools/cli/src/cli/recycle.rs +++ b/tools/cli/src/cli/recycle.rs @@ -16,7 +16,7 @@ impl RecycleOpt { let client = copt.to_client(OpType::Read).await; match client.recycle_bin_list().await { Ok(r) => r.iter().for_each(|e| println!("{}", e)), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } RecycleOpt::Get(nopt) => { @@ -24,13 +24,13 @@ impl RecycleOpt { match client.recycle_bin_get(nopt.name.as_str()).await { Ok(Some(e)) => println!("{}", e), Ok(None) => println!("No matching entries"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } RecycleOpt::Revive(nopt) => { let client = nopt.copt.to_client(OpType::Write).await; if let Err(e) = client.recycle_bin_revive(nopt.name.as_str()).await { - handle_client_error(e, &nopt.copt.output_mode) + handle_client_error(e, nopt.copt.output_mode) } } } diff --git a/tools/cli/src/cli/serviceaccount.rs b/tools/cli/src/cli/serviceaccount.rs index 64f5dea9d..589c9f8f7 100644 --- a/tools/cli/src/cli/serviceaccount.rs +++ b/tools/cli/src/cli/serviceaccount.rs @@ -192,7 +192,7 @@ impl ServiceAccountOpt { .await { Ok(token) => println!("{}", token), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } ServiceAccountPosix::Set(aopt) => { @@ -205,7 +205,7 @@ impl ServiceAccountOpt { ) .await { - handle_client_error(e, &aopt.copt.output_mode) + handle_client_error(e, aopt.copt.output_mode) } } }, // end ServiceAccountOpt::Posix @@ -258,7 +258,7 @@ impl ServiceAccountOpt { .await { Ok(pkeys) => pkeys.iter().for_each(|pkey| println!("{}", pkey)), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } AccountSsh::Add(aopt) => { @@ -271,7 +271,7 @@ impl ServiceAccountOpt { ) .await { - handle_client_error(e, &aopt.copt.output_mode) + handle_client_error(e, aopt.copt.output_mode) } } AccountSsh::Delete(aopt) => { @@ -283,7 +283,7 @@ impl ServiceAccountOpt { ) .await { - handle_client_error(e, &aopt.copt.output_mode) + handle_client_error(e, aopt.copt.output_mode) } } }, // end ServiceAccountOpt::Ssh @@ -291,7 +291,7 @@ impl ServiceAccountOpt { let client = copt.to_client(OpType::Read).await; match client.idm_service_account_list().await { Ok(r) => r.iter().for_each(|ent| println!("{}", ent)), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } ServiceAccountOpt::Update(aopt) => { @@ -306,18 +306,18 @@ impl ServiceAccountOpt { .await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } ServiceAccountOpt::Get(aopt) => { let client = aopt.copt.to_client(OpType::Read).await; - match client + let res = client .idm_service_account_get(aopt.aopts.account_id.as_str()) - .await - { - Ok(Some(e)) => println!("{}", e), + .await; + match res { + Ok(Some(e)) => aopt.copt.output_mode.print_message(e), Ok(None) => println!("No matching entries"), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } ServiceAccountOpt::Delete(aopt) => { @@ -358,7 +358,7 @@ impl ServiceAccountOpt { ) .await { - handle_client_error(e, &acopt.copt.output_mode) + handle_client_error(e, acopt.copt.output_mode) } } ServiceAccountOpt::Validity { commands } => match commands { @@ -447,7 +447,7 @@ impl ServiceAccountOpt { } }; match res { - Err(e) => handle_client_error(e, &ano.copt.output_mode), + Err(e) => handle_client_error(e, ano.copt.output_mode), _ => println!("Success"), }; } @@ -462,7 +462,7 @@ impl ServiceAccountOpt { ) .await { - Err(e) => handle_client_error(e, &ano.copt.output_mode), + Err(e) => handle_client_error(e, ano.copt.output_mode), _ => println!("Success"), } } else { @@ -480,7 +480,7 @@ impl ServiceAccountOpt { ) .await { - Err(e) => handle_client_error(e, &ano.copt.output_mode), + Err(e) => handle_client_error(e, ano.copt.output_mode), _ => println!("Success"), } } @@ -494,7 +494,7 @@ impl ServiceAccountOpt { .await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &aopt.copt.output_mode), + Err(e) => handle_client_error(e, aopt.copt.output_mode), } } } diff --git a/tools/cli/src/cli/session.rs b/tools/cli/src/cli/session.rs index 55797ba17..1f45546e3 100644 --- a/tools/cli/src/cli/session.rs +++ b/tools/cli/src/cli/session.rs @@ -1,7 +1,7 @@ use crate::common::OpType; use std::collections::BTreeMap; use std::fs::{create_dir, File}; -use std::io::{self, BufReader, BufWriter, ErrorKind, Write}; +use std::io::{self, BufReader, BufWriter, ErrorKind, IsTerminal, Write}; use std::path::PathBuf; use std::str::FromStr; @@ -9,6 +9,7 @@ use compact_jwt::{Jws, JwsUnverified}; use dialoguer::theme::ColorfulTheme; use dialoguer::Select; use kanidm_client::{ClientError, KanidmClient}; +use kanidm_proto::constants::CLIENT_TOKEN_CACHE; use kanidm_proto::v1::{AuthAllowed, AuthResponse, AuthState, UserAuthToken}; #[cfg(target_family = "unix")] use libc::umask; @@ -16,18 +17,26 @@ use webauthn_authenticator_rs::prelude::RequestChallengeResponse; use crate::common::prompt_for_username_get_username; use crate::webauthn::get_authenticator; -use crate::{LoginOpt, LogoutOpt, ReauthOpt, SessionOpt}; +use crate::{CommonOpt, LoginOpt, LogoutOpt, ReauthOpt, SessionOpt}; static TOKEN_DIR: &str = "~/.cache"; -static TOKEN_PATH: &str = "~/.cache/kanidm_tokens"; + +impl CommonOpt { + fn get_token_cache_path(&self) -> String { + match self.token_cache_path.clone() { + None => CLIENT_TOKEN_CACHE.to_string(), + Some(val) => val.clone(), + } + } +} #[allow(clippy::result_unit_err)] -pub fn read_tokens() -> Result, ()> { - let token_path = PathBuf::from(shellexpand::tilde(TOKEN_PATH).into_owned()); +pub fn read_tokens(token_path: &str) -> Result, ()> { + let token_path = PathBuf::from(shellexpand::tilde(token_path).into_owned()); if !token_path.exists() { debug!( "Token cache file path {:?} does not exist, returning an empty token store.", - TOKEN_PATH + token_path ); return Ok(BTreeMap::new()); } @@ -50,7 +59,8 @@ pub fn read_tokens() -> Result, ()> { _ => { warn!( "Cannot read tokens from {} due to error: {:?} ... continuing.", - TOKEN_PATH, e + token_path.display(), + e ); return Ok(BTreeMap::new()); } @@ -69,9 +79,9 @@ pub fn read_tokens() -> Result, ()> { } #[allow(clippy::result_unit_err)] -pub fn write_tokens(tokens: &BTreeMap) -> Result<(), ()> { +pub fn write_tokens(tokens: &BTreeMap, token_path: &str) -> Result<(), ()> { let token_dir = PathBuf::from(shellexpand::tilde(TOKEN_DIR).into_owned()); - let token_path = PathBuf::from(shellexpand::tilde(TOKEN_PATH).into_owned()); + let token_path = PathBuf::from(shellexpand::tilde(token_path).into_owned()); token_dir .parent() @@ -103,7 +113,7 @@ pub fn write_tokens(tokens: &BTreeMap) -> Result<(), ()> { let file = File::create(&token_path).map_err(|e| { #[cfg(target_family = "unix")] let _ = unsafe { umask(before) }; - error!("Can not write to {} -> {:?}", TOKEN_PATH, e); + error!("Can not write to {} -> {:?}", token_path.display(), e); })?; #[cfg(target_family = "unix")] @@ -301,7 +311,7 @@ async fn process_auth_state( } // Read the current tokens - let mut tokens = read_tokens().unwrap_or_else(|_| { + let mut tokens = read_tokens(&client.get_token_cache_path()).unwrap_or_else(|_| { error!("Error retrieving authentication token store"); std::process::exit(1); }); @@ -333,7 +343,7 @@ async fn process_auth_state( tokens.insert(spn.clone(), tonk); // write them out. - if write_tokens(&tokens).is_err() { + if write_tokens(&tokens, &client.get_token_cache_path()).is_err() { error!("Error persisting authentication token store"); std::process::exit(1); }; @@ -436,13 +446,21 @@ impl LogoutOpt { let mut _tmp_username = String::new(); match &self.copt.username { Some(value) => value.clone(), - None => match prompt_for_username_get_username() { - Ok(value) => value, - Err(msg) => { - error!("{}", msg); - std::process::exit(1); + None => { + // check if we're in a tty + if std::io::stdin().is_terminal() { + match prompt_for_username_get_username(&self.copt.get_token_cache_path()) { + Ok(value) => value, + Err(msg) => { + error!("{}", msg); + std::process::exit(1); + } + } + } else { + eprintln!("Not running in interactive mode and no username specified, can't continue!"); + return; } - }, + } } } else { let client = self.copt.to_client(OpType::Read).await; @@ -480,7 +498,7 @@ impl LogoutOpt { uat.spn }; - let mut tokens = read_tokens().unwrap_or_else(|_| { + let mut tokens = read_tokens(&self.copt.get_token_cache_path()).unwrap_or_else(|_| { error!("Error retrieving authentication token store"); std::process::exit(1); }); @@ -488,7 +506,7 @@ impl LogoutOpt { // Remove our old one if tokens.remove(&spn).is_some() { // write them out. - if let Err(_e) = write_tokens(&tokens) { + if let Err(_e) = write_tokens(&tokens, &self.copt.get_token_cache_path()) { error!("Error persisting authentication token store"); std::process::exit(1); }; @@ -506,8 +524,8 @@ impl SessionOpt { } } - fn read_valid_tokens() -> BTreeMap { - read_tokens() + fn read_valid_tokens(token_cache_path: &str) -> BTreeMap { + read_tokens(token_cache_path) .unwrap_or_else(|_| { error!("Error retrieving authentication token store"); std::process::exit(1); @@ -535,15 +553,15 @@ impl SessionOpt { pub async fn exec(&self) { match self { - SessionOpt::List(_) => { - let tokens = Self::read_valid_tokens(); + SessionOpt::List(copt) => { + let tokens = Self::read_valid_tokens(&copt.get_token_cache_path()); for (_, uat) in tokens.values() { println!("---"); println!("{}", uat); } } - SessionOpt::Cleanup(_) => { - let tokens = Self::read_valid_tokens(); + SessionOpt::Cleanup(copt) => { + let tokens = Self::read_valid_tokens(&copt.get_token_cache_path()); let start_len = tokens.len(); let now = time::OffsetDateTime::now_utc(); @@ -566,7 +584,7 @@ impl SessionOpt { let end_len = tokens.len(); - if let Err(_e) = write_tokens(&tokens) { + if let Err(_e) = write_tokens(&tokens, &copt.get_token_cache_path()) { error!("Error persisting authentication token store"); std::process::exit(1); }; diff --git a/tools/cli/src/cli/synch.rs b/tools/cli/src/cli/synch.rs index 733b00b2f..e7761ed20 100644 --- a/tools/cli/src/cli/synch.rs +++ b/tools/cli/src/cli/synch.rs @@ -25,7 +25,7 @@ impl SynchOpt { match client.idm_sync_account_list().await { Ok(r) => r.iter().for_each(|ent| println!("{}", ent)), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } SynchOpt::Get(nopt) => { @@ -33,7 +33,7 @@ impl SynchOpt { match client.idm_sync_account_get(nopt.name.as_str()).await { Ok(Some(e)) => println!("{}", e), Ok(None) => println!("No matching entries"), - Err(e) => handle_client_error(e, &nopt.copt.output_mode), + Err(e) => handle_client_error(e, nopt.copt.output_mode), } } SynchOpt::SetCredentialPortal { @@ -47,7 +47,7 @@ impl SynchOpt { .await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } SynchOpt::Create { @@ -61,7 +61,7 @@ impl SynchOpt { .await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } SynchOpt::GenerateToken { @@ -75,14 +75,14 @@ impl SynchOpt { .await { Ok(token) => println!("token: {}", token), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } SynchOpt::DestroyToken { account_id, copt } => { let client = copt.to_client(OpType::Write).await; match client.idm_sync_account_destroy_token(account_id).await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } SynchOpt::SetYieldAttributes { @@ -96,14 +96,14 @@ impl SynchOpt { .await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } SynchOpt::ForceRefresh { account_id, copt } => { let client = copt.to_client(OpType::Write).await; match client.idm_sync_account_force_refresh(account_id).await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } SynchOpt::Finalise { account_id, copt } => { @@ -120,7 +120,7 @@ impl SynchOpt { let client = copt.to_client(OpType::Write).await; match client.idm_sync_account_finalise(account_id).await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } SynchOpt::Terminate { account_id, copt } => { @@ -137,7 +137,7 @@ impl SynchOpt { let client = copt.to_client(OpType::Write).await; match client.idm_sync_account_terminate(account_id).await { Ok(()) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } } diff --git a/tools/cli/src/cli/system_config/badlist.rs b/tools/cli/src/cli/system_config/badlist.rs index 6d869bb81..69a5edb66 100644 --- a/tools/cli/src/cli/system_config/badlist.rs +++ b/tools/cli/src/cli/system_config/badlist.rs @@ -30,7 +30,7 @@ impl PwBadlistOpt { eprintln!("--"); eprintln!("Success"); } - Err(e) => crate::handle_client_error(e, &copt.output_mode), + Err(e) => crate::handle_client_error(e, copt.output_mode), } } PwBadlistOpt::Upload { @@ -126,7 +126,7 @@ impl PwBadlistOpt { let client = copt.to_client(OpType::Write).await; match client.system_password_badlist_append(filt_pwset).await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } } // End Upload @@ -165,7 +165,7 @@ impl PwBadlistOpt { match client.system_password_badlist_remove(pwset).await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } // End Remove } diff --git a/tools/cli/src/cli/system_config/denied_names.rs b/tools/cli/src/cli/system_config/denied_names.rs index c29b8ce6c..adc02302d 100644 --- a/tools/cli/src/cli/system_config/denied_names.rs +++ b/tools/cli/src/cli/system_config/denied_names.rs @@ -23,7 +23,7 @@ impl DeniedNamesOpt { eprintln!("--"); eprintln!("Success"); } - Err(e) => crate::handle_client_error(e, &copt.output_mode), + Err(e) => crate::handle_client_error(e, copt.output_mode), } } DeniedNamesOpt::Append { copt, names } => { @@ -31,7 +31,7 @@ impl DeniedNamesOpt { match client.system_denied_names_append(names).await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } DeniedNamesOpt::Remove { copt, names } => { @@ -39,7 +39,7 @@ impl DeniedNamesOpt { match client.system_denied_names_remove(names).await { Ok(_) => println!("Success"), - Err(e) => handle_client_error(e, &copt.output_mode), + Err(e) => handle_client_error(e, copt.output_mode), } } } diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index eed0059d2..c8cdca6f6 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -14,7 +14,7 @@ pub struct DebugOpt { pub debug: bool, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] /// The CLI output mode, either text or json, falls back to text if you ask for something other than text/json pub enum OutputMode { Text, @@ -32,6 +32,25 @@ impl std::str::FromStr for OutputMode { } } +impl OutputMode { + pub fn print_message(self, input: T) + where + T: serde::Serialize + std::fmt::Debug + std::fmt::Display, + { + match self { + OutputMode::Json => { + println!( + "{}", + serde_json::to_string(&input).unwrap_or(format!("{:?}", input)) + ); + } + OutputMode::Text => { + println!("{}", input); + } + } + } +} + #[derive(Debug, Args, Clone)] pub struct CommonOpt { /// Enable debugging of the kanidm tool @@ -56,6 +75,9 @@ pub struct CommonOpt { default_value_t = false )] skip_hostname_verification: bool, + /// Path to a file to cache tokens in, defaults to ~/.cache/kanidm_tokens + #[clap(short, long, env = "KANIDM_TOKEN_CACHE_PATH", hide = true, default_value = None)] + token_cache_path: Option, } #[derive(Debug, Args)] @@ -154,8 +176,8 @@ pub enum GroupOpt { #[clap(name = "account-policy")] AccountPolicy { #[clap(subcommand)] - commands: GroupAccountPolicyOpt - } + commands: GroupAccountPolicyOpt, + }, } #[derive(Debug, Args)] @@ -597,10 +619,10 @@ pub struct LogoutOpt { pub enum SessionOpt { #[clap(name = "list")] /// List current active sessions - List(DebugOpt), + List(CommonOpt), #[clap(name = "cleanup")] /// Remove sessions that have expired or are invalid. - Cleanup(DebugOpt), + Cleanup(CommonOpt), } #[derive(Debug, Args)] @@ -853,7 +875,6 @@ pub enum DeniedNamesOpt { }, } - #[derive(Debug, Subcommand)] pub enum DomainOpt { #[clap[name = "set-display-name"]]