Add support for better client building (#147)

Implements #134 Client Builder Pattern. This makes it much easier to build a client by making the configuration of the client lib follow a builder pattern. The error management needs a lot of work still, but for now it's rough and it works.
This commit is contained in:
Firstyear 2019-11-19 12:20:37 +10:30 committed by GitHub
parent 44693be17a
commit 6157c65d3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 238 additions and 45 deletions

View file

@ -11,6 +11,8 @@ reqwest = "0.9"
kanidm_proto = { path = "../kanidm_proto" } kanidm_proto = { path = "../kanidm_proto" }
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_derive = "1.0"
toml = "0.5"
[dev-dependencies] [dev-dependencies]
tokio = "0.1" tokio = "0.1"

View file

@ -7,9 +7,12 @@ extern crate log;
use reqwest; use reqwest;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use serde_derive::Deserialize;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path::Path;
use toml;
use kanidm_proto::v1::{ use kanidm_proto::v1::{
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest, AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest,
@ -29,61 +32,195 @@ pub enum ClientError {
EmptyResponse, EmptyResponse,
} }
#[derive(Debug)] #[derive(Debug, Deserialize)]
pub struct KanidmClient { struct KanidmClientConfig {
client: reqwest::Client, uri: Option<String>,
addr: String, verify_ca: Option<bool>,
verify_hostnames: Option<bool>,
ca_path: Option<String>,
// Should we add username/pw later? They could be part of the builder
// process ...
}
#[derive(Debug, Clone)]
pub struct KanidmClientBuilder {
address: Option<String>,
verify_ca: bool,
verify_hostnames: bool,
ca: Option<reqwest::Certificate>, ca: Option<reqwest::Certificate>,
} }
impl KanidmClient { impl KanidmClientBuilder {
pub fn new(addr: &str, ca: Option<&str>) -> Self { pub fn new() -> Self {
let ca = ca.map(|ca_path| { KanidmClientBuilder {
//Okay we have a ca to add. Let's read it in and setup. address: None,
verify_ca: true,
verify_hostnames: true,
ca: None,
}
}
fn parse_certificate(ca_path: &str) -> Result<reqwest::Certificate, ()> {
let mut buf = Vec::new(); let mut buf = Vec::new();
// TODO: Better than expect? // TODO: Handle these errors better, or at least provide diagnostics?
let mut f = File::open(ca_path).expect("Failed to open ca"); let mut f = File::open(ca_path).map_err(|_| ())?;
f.read_to_end(&mut buf).expect("Failed to read ca"); f.read_to_end(&mut buf).map_err(|_| ())?;
reqwest::Certificate::from_pem(&buf).expect("Failed to parse ca") reqwest::Certificate::from_pem(&buf).map_err(|_| ())
}); }
let client = Self::build_reqwest(&ca).expect("Unexpected reqwest builder failure!"); fn apply_config_options(self, kcc: KanidmClientConfig) -> Result<Self, ()> {
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,
};
KanidmClient { Ok(KanidmClientBuilder {
client: client, address: address,
addr: addr.to_string(), verify_ca: verify_ca,
verify_hostnames: verify_hostnames,
ca: ca, ca: ca,
})
}
pub fn read_options_from_optional_config<P: AsRef<Path>>(
self,
config_path: P,
) -> Result<Self, ()> {
// 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 { pub fn danger_accept_invalid_hostnames(self, accept_invalid_hostnames: bool) -> Self {
let new_client = KanidmClientBuilder {
Self::build_reqwest(&self.ca).expect("Unexpected reqwest builder failure!"); address: self.address,
verify_ca: self.verify_ca,
KanidmClient { // We have to flip the bool state here due to english language.
client: new_client, verify_hostnames: !accept_invalid_hostnames,
addr: self.addr.clone(), ca: self.ca,
ca: self.ca.clone(),
} }
} }
fn build_reqwest(ca: &Option<reqwest::Certificate>) -> Result<reqwest::Client, reqwest::Error> { 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<Self, ()> {
//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<KanidmClient, reqwest::Error> {
// 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() let client_builder = reqwest::Client::builder()
.cookie_store(true); .cookie_store(true)
// .danger_accept_invalid_hostnames(true) .danger_accept_invalid_hostnames(!self.verify_hostnames)
// .danger_accept_invalid_certs(true); .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()), Some(cert) => client_builder.add_root_certificate(cert.clone()),
None => client_builder, 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<Self, reqwest::Error> {
// Copy our builder, and then just process it.
let builder = self.builder.clone();
builder.build()
} }
pub fn logout(&mut self) -> Result<(), reqwest::Error> { pub fn logout(&mut self) -> Result<(), reqwest::Error> {
let mut r_client = Self::build_reqwest(&self.ca)?; // hack - we have to replace our reqwest client because that's the only way
std::mem::swap(&mut self.client, &mut r_client); // 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(()) Ok(())
} }

View file

@ -11,7 +11,7 @@ extern crate kanidm_client;
extern crate kanidm_proto; extern crate kanidm_proto;
extern crate serde_json; extern crate serde_json;
use kanidm_client::KanidmClient; use kanidm_client::{KanidmClient, KanidmClientBuilder};
use kanidm::config::{Configuration, IntegrationTestConfig}; use kanidm::config::{Configuration, IntegrationTestConfig};
use kanidm::core::create_server_core; 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. // Setup the client, and the address we selected.
let addr = format!("http://127.0.0.1:{}", port); 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); 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"); let res = rsclient.idm_account_primary_credential_set_password("testperson", "password");
assert!(res.is_ok()); assert!(res.is_ok());
// Check it stuck. // Check it stuck.
let tclient = rsclient.new_session(); let tclient = rsclient.new_session().expect("failed to build new session");
assert!(tclient assert!(tclient
.auth_simple_password("testperson", "password") .auth_simple_password("testperson", "password")
.is_ok()); .is_ok());
@ -268,7 +271,7 @@ fn test_server_admin_reset_simple_password() {
let res = rsclient.idm_account_primary_credential_set_generated("testperson"); let res = rsclient.idm_account_primary_credential_set_generated("testperson");
assert!(res.is_ok()); assert!(res.is_ok());
let gpw = res.unwrap(); let gpw = res.unwrap();
let tclient = rsclient.new_session(); let tclient = rsclient.new_session().expect("failed to build new session");
assert!(tclient assert!(tclient
.auth_simple_password("testperson", gpw.as_str()) .auth_simple_password("testperson", gpw.as_str())
.is_ok()); .is_ok());

View file

@ -20,7 +20,7 @@ rpassword = "0.4"
structopt = { version = "0.2", default-features = false } structopt = { version = "0.2", default-features = false }
log = "0.4" log = "0.4"
env_logger = "0.6" env_logger = "0.6"
toml = "0.5"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
shellexpand = "1.0"

View file

@ -1,10 +1,11 @@
extern crate structopt; extern crate structopt;
use kanidm_client::KanidmClient; use kanidm_client::{KanidmClient, KanidmClientBuilder};
use kanidm_proto::v1::{Entry, Filter, Modify, ModifyList}; use kanidm_proto::v1::{Entry, Filter, Modify, ModifyList};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt; use structopt::StructOpt;
use shellexpand;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::error::Error; use std::error::Error;
use std::fs::File; use std::fs::File;
@ -20,7 +21,7 @@ struct CommonOpt {
#[structopt(short = "d", long = "debug")] #[structopt(short = "d", long = "debug")]
debug: bool, debug: bool,
#[structopt(short = "H", long = "url")] #[structopt(short = "H", long = "url")]
addr: String, addr: Option<String>,
#[structopt(short = "D", long = "name")] #[structopt(short = "D", long = "name")]
username: String, username: String,
#[structopt(parse(from_os_str), short = "C", long = "ca")] #[structopt(parse(from_os_str), short = "C", long = "ca")]
@ -29,8 +30,33 @@ struct CommonOpt {
impl CommonOpt { impl CommonOpt {
fn to_client(&self) -> KanidmClient { 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 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" { let r = if self.username == "anonymous" {
client.auth_anonymous() client.auth_anonymous()

View file

@ -1,5 +1,6 @@
extern crate structopt; extern crate structopt;
use kanidm_client::KanidmClient; use kanidm_client::KanidmClientBuilder;
use shellexpand;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt; use structopt::StructOpt;
@ -12,7 +13,7 @@ struct ClientOpt {
#[structopt(short = "d", long = "debug")] #[structopt(short = "d", long = "debug")]
debug: bool, debug: bool,
#[structopt(short = "H", long = "url")] #[structopt(short = "H", long = "url")]
addr: String, addr: Option<String>,
#[structopt(short = "D", long = "name")] #[structopt(short = "D", long = "name")]
username: String, username: String,
#[structopt(parse(from_os_str), short = "C", long = "ca")] #[structopt(parse(from_os_str), short = "C", long = "ca")]
@ -34,8 +35,32 @@ fn main() {
} }
env_logger::init(); 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 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" { let r = if opt.username == "anonymous" {
client.auth_anonymous() client.auth_anonymous()