mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
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:
parent
44693be17a
commit
6157c65d3a
|
@ -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"
|
||||
|
|
|
@ -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<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>,
|
||||
}
|
||||
|
||||
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<reqwest::Certificate, ()> {
|
||||
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<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,
|
||||
};
|
||||
|
||||
Ok(KanidmClientBuilder {
|
||||
address: address,
|
||||
verify_ca: verify_ca,
|
||||
verify_hostnames: verify_hostnames,
|
||||
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 {
|
||||
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<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()
|
||||
.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<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> {
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
#[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()
|
||||
|
|
|
@ -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<String>,
|
||||
#[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()
|
||||
|
|
Loading…
Reference in a new issue