diff --git a/kanidm_client/Cargo.toml b/kanidm_client/Cargo.toml index e762759d7..b1298e4c9 100644 --- a/kanidm_client/Cargo.toml +++ b/kanidm_client/Cargo.toml @@ -11,6 +11,8 @@ reqwest = "0.9" kanidm_proto = { path = "../kanidm_proto" } serde = "1.0" serde_json = "1.0" +serde_derive = "1.0" +toml = "0.5" [dev-dependencies] tokio = "0.1" diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index 9c73c6e12..c464db433 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -7,9 +7,12 @@ extern crate log; use reqwest; use serde::de::DeserializeOwned; use serde::Serialize; +use serde_derive::Deserialize; use std::collections::BTreeMap; use std::fs::File; use std::io::Read; +use std::path::Path; +use toml; use kanidm_proto::v1::{ AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest, @@ -29,61 +32,195 @@ pub enum ClientError { EmptyResponse, } -#[derive(Debug)] -pub struct KanidmClient { - client: reqwest::Client, - addr: String, +#[derive(Debug, Deserialize)] +struct KanidmClientConfig { + uri: Option, + verify_ca: Option, + verify_hostnames: Option, + ca_path: Option, + // Should we add username/pw later? They could be part of the builder + // process ... +} + +#[derive(Debug, Clone)] +pub struct KanidmClientBuilder { + address: Option, + verify_ca: bool, + verify_hostnames: bool, ca: Option, } -impl KanidmClient { - pub fn new(addr: &str, ca: Option<&str>) -> Self { - let ca = ca.map(|ca_path| { - //Okay we have a ca to add. Let's read it in and setup. - let mut buf = Vec::new(); - // TODO: Better than expect? - let mut f = File::open(ca_path).expect("Failed to open ca"); - f.read_to_end(&mut buf).expect("Failed to read ca"); - reqwest::Certificate::from_pem(&buf).expect("Failed to parse ca") - }); +impl KanidmClientBuilder { + pub fn new() -> Self { + KanidmClientBuilder { + address: None, + verify_ca: true, + verify_hostnames: true, + ca: None, + } + } - let client = Self::build_reqwest(&ca).expect("Unexpected reqwest builder failure!"); + fn parse_certificate(ca_path: &str) -> Result { + let mut buf = Vec::new(); + // TODO: Handle these errors better, or at least provide diagnostics? + let mut f = File::open(ca_path).map_err(|_| ())?; + f.read_to_end(&mut buf).map_err(|_| ())?; + reqwest::Certificate::from_pem(&buf).map_err(|_| ()) + } - KanidmClient { - client: client, - addr: addr.to_string(), + fn apply_config_options(self, kcc: KanidmClientConfig) -> Result { + let KanidmClientBuilder { + address, + verify_ca, + verify_hostnames, + ca, + } = self; + // Process and apply all our options if they exist. + let address = match kcc.uri { + Some(uri) => Some(uri), + None => address, + }; + let verify_ca = kcc.verify_ca.unwrap_or_else(|| verify_ca); + let verify_hostnames = kcc.verify_hostnames.unwrap_or_else(|| verify_hostnames); + let ca = match kcc.ca_path { + Some(ca_path) => Some(Self::parse_certificate(ca_path.as_str())?), + None => ca, + }; + + Ok(KanidmClientBuilder { + address: address, + verify_ca: verify_ca, + verify_hostnames: verify_hostnames, ca: ca, + }) + } + + pub fn read_options_from_optional_config>( + self, + config_path: P, + ) -> Result { + // If the file does not exist, we skip this function. + let mut f = match File::open(config_path) { + Ok(f) => f, + Err(e) => { + debug!("Unabled to open config file [{:?}], skipping ...", e); + return Ok(self); + } + }; + + let mut contents = String::new(); + f.read_to_string(&mut contents).map_err(|e| { + eprintln!("{:?}", e); + () + })?; + + let config: KanidmClientConfig = toml::from_str(contents.as_str()).map_err(|e| { + eprintln!("{:?}", e); + () + })?; + + self.apply_config_options(config) + } + + pub fn address(self, address: String) -> Self { + KanidmClientBuilder { + address: Some(address), + verify_ca: self.verify_ca, + verify_hostnames: self.verify_hostnames, + ca: self.ca, } } - pub fn new_session(&self) -> Self { - let new_client = - Self::build_reqwest(&self.ca).expect("Unexpected reqwest builder failure!"); - - KanidmClient { - client: new_client, - addr: self.addr.clone(), - ca: self.ca.clone(), + 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, } } - fn build_reqwest(ca: &Option) -> Result { + 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, + } + } + + pub fn add_root_certificate_filepath(self, ca_path: &str) -> Result { + //Okay we have a ca to add. Let's read it in and setup. + let ca = Self::parse_certificate(ca_path)?; + + Ok(KanidmClientBuilder { + address: self.address, + verify_ca: self.verify_ca, + verify_hostnames: self.verify_hostnames, + ca: Some(ca), + }) + } + + // Consume self and return a client. + pub fn build(self) -> Result { + // Errghh, how to handle this cleaner. + let address = match &self.address { + Some(a) => a.clone(), + None => { + eprintln!("uri (-H) missing, can not proceed"); + unimplemented!(); + } + }; + let client_builder = reqwest::Client::builder() - .cookie_store(true); - // .danger_accept_invalid_hostnames(true) - // .danger_accept_invalid_certs(true); + .cookie_store(true) + .danger_accept_invalid_hostnames(!self.verify_hostnames) + .danger_accept_invalid_certs(!self.verify_ca); - let client_builder = match ca { + let client_builder = match &self.ca { Some(cert) => client_builder.add_root_certificate(cert.clone()), None => client_builder, }; - client_builder.build() + let client = client_builder.build()?; + + Ok(KanidmClient { + client: client, + addr: address, + builder: self, + }) + } +} + +#[derive(Debug)] +pub struct KanidmClient { + client: reqwest::Client, + addr: String, + builder: KanidmClientBuilder, +} + +impl KanidmClient { + pub fn new_session(&self) -> Result { + // Copy our builder, and then just process it. + let builder = self.builder.clone(); + builder.build() } pub fn logout(&mut self) -> Result<(), reqwest::Error> { - let mut r_client = Self::build_reqwest(&self.ca)?; - std::mem::swap(&mut self.client, &mut r_client); + // hack - we have to replace our reqwest client because that's the only way + // to currently flush the cookie store. To achieve this we need to rebuild + // and then destructure. + + let builder = self.builder.clone(); + let KanidmClient { + mut client, + addr: _, + builder: _, + } = builder.build()?; + + std::mem::swap(&mut self.client, &mut client); Ok(()) } diff --git a/kanidm_client/tests/proto_v1_test.rs b/kanidm_client/tests/proto_v1_test.rs index 77897b92d..755a3df31 100644 --- a/kanidm_client/tests/proto_v1_test.rs +++ b/kanidm_client/tests/proto_v1_test.rs @@ -11,7 +11,7 @@ extern crate kanidm_client; extern crate kanidm_proto; extern crate serde_json; -use kanidm_client::KanidmClient; +use kanidm_client::{KanidmClient, KanidmClientBuilder}; use kanidm::config::{Configuration, IntegrationTestConfig}; use kanidm::core::create_server_core; @@ -72,7 +72,10 @@ fn run_test(test_fn: fn(KanidmClient) -> ()) { // Setup the client, and the address we selected. let addr = format!("http://127.0.0.1:{}", port); - let rsclient = KanidmClient::new(addr.as_str(), None); + let rsclient = KanidmClientBuilder::new() + .address(addr) + .build() + .expect("Failed to build client"); test_fn(rsclient); @@ -259,7 +262,7 @@ fn test_server_admin_reset_simple_password() { let res = rsclient.idm_account_primary_credential_set_password("testperson", "password"); assert!(res.is_ok()); // Check it stuck. - let tclient = rsclient.new_session(); + let tclient = rsclient.new_session().expect("failed to build new session"); assert!(tclient .auth_simple_password("testperson", "password") .is_ok()); @@ -268,7 +271,7 @@ fn test_server_admin_reset_simple_password() { let res = rsclient.idm_account_primary_credential_set_generated("testperson"); assert!(res.is_ok()); let gpw = res.unwrap(); - let tclient = rsclient.new_session(); + let tclient = rsclient.new_session().expect("failed to build new session"); assert!(tclient .auth_simple_password("testperson", gpw.as_str()) .is_ok()); diff --git a/kanidm_tools/Cargo.toml b/kanidm_tools/Cargo.toml index 899327590..1b39d5310 100644 --- a/kanidm_tools/Cargo.toml +++ b/kanidm_tools/Cargo.toml @@ -20,7 +20,7 @@ rpassword = "0.4" structopt = { version = "0.2", default-features = false } log = "0.4" env_logger = "0.6" -toml = "0.5" serde = "1.0" serde_json = "1.0" +shellexpand = "1.0" diff --git a/kanidm_tools/src/main.rs b/kanidm_tools/src/main.rs index a3546b93b..cea9a1535 100644 --- a/kanidm_tools/src/main.rs +++ b/kanidm_tools/src/main.rs @@ -1,10 +1,11 @@ extern crate structopt; -use kanidm_client::KanidmClient; +use kanidm_client::{KanidmClient, KanidmClientBuilder}; use kanidm_proto::v1::{Entry, Filter, Modify, ModifyList}; use serde::de::DeserializeOwned; use std::path::PathBuf; use structopt::StructOpt; +use shellexpand; use std::collections::BTreeMap; use std::error::Error; use std::fs::File; @@ -20,7 +21,7 @@ struct CommonOpt { #[structopt(short = "d", long = "debug")] debug: bool, #[structopt(short = "H", long = "url")] - addr: String, + addr: Option, #[structopt(short = "D", long = "name")] username: String, #[structopt(parse(from_os_str), short = "C", long = "ca")] @@ -29,8 +30,33 @@ struct CommonOpt { impl CommonOpt { fn to_client(&self) -> KanidmClient { + let config_path: String = shellexpand::tilde("~/.config/kanidm").into_owned(); + + debug!("Attempting to use config {}", "/etc/kanidm/config"); + let client_builder = KanidmClientBuilder::new() + .read_options_from_optional_config("/etc/kanidm/config") + .and_then(|cb| { + debug!("Attempting to use config {}", config_path); + cb.read_options_from_optional_config(config_path) + }) + .expect("Failed to parse config (if present)"); + + let client_builder = match &self.addr { + Some(a) => client_builder.address(a.to_string()), + None => client_builder, + }; + let ca_path: Option<&str> = self.ca_path.as_ref().map(|p| p.to_str().unwrap()); - let client = KanidmClient::new(self.addr.as_str(), ca_path); + let client_builder = match ca_path { + Some(p) => client_builder + .add_root_certificate_filepath(p) + .expect("Failed to access CA file"), + None => client_builder, + }; + + let client = client_builder + .build() + .expect("Failed to build client instance"); let r = if self.username == "anonymous" { client.auth_anonymous() diff --git a/kanidm_tools/src/ssh_authorizedkeys.rs b/kanidm_tools/src/ssh_authorizedkeys.rs index 7f55a50c0..883a4e60c 100644 --- a/kanidm_tools/src/ssh_authorizedkeys.rs +++ b/kanidm_tools/src/ssh_authorizedkeys.rs @@ -1,5 +1,6 @@ extern crate structopt; -use kanidm_client::KanidmClient; +use kanidm_client::KanidmClientBuilder; +use shellexpand; use std::path::PathBuf; use structopt::StructOpt; @@ -12,7 +13,7 @@ struct ClientOpt { #[structopt(short = "d", long = "debug")] debug: bool, #[structopt(short = "H", long = "url")] - addr: String, + addr: Option, #[structopt(short = "D", long = "name")] username: String, #[structopt(parse(from_os_str), short = "C", long = "ca")] @@ -34,8 +35,32 @@ fn main() { } env_logger::init(); + let config_path: String = shellexpand::tilde("~/.config/kanidm").into_owned(); + debug!("Attempting to use config {}", "/etc/kanidm/config"); + let client_builder = KanidmClientBuilder::new() + .read_options_from_optional_config("/etc/kanidm/config") + .and_then(|cb| { + debug!("Attempting to use config {}", config_path); + cb.read_options_from_optional_config(config_path) + }) + .expect("Failed to parse config (if present)"); + + let client_builder = match &opt.addr { + Some(a) => client_builder.address(a.to_string()), + None => client_builder, + }; + let ca_path: Option<&str> = opt.ca_path.as_ref().map(|p| p.to_str().unwrap()); - let client = KanidmClient::new(opt.addr.as_str(), ca_path); + let client_builder = match ca_path { + Some(p) => client_builder + .add_root_certificate_filepath(p) + .expect("Failed to access CA file"), + None => client_builder, + }; + + let client = client_builder + .build() + .expect("Failed to build client instance"); let r = if opt.username == "anonymous" { client.auth_anonymous()