mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +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" }
|
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"
|
||||||
|
|
|
@ -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,
|
||||||
let mut buf = Vec::new();
|
verify_ca: true,
|
||||||
// TODO: Better than expect?
|
verify_hostnames: true,
|
||||||
let mut f = File::open(ca_path).expect("Failed to open ca");
|
ca: None,
|
||||||
f.read_to_end(&mut buf).expect("Failed to read ca");
|
}
|
||||||
reqwest::Certificate::from_pem(&buf).expect("Failed to parse ca")
|
}
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
fn apply_config_options(self, kcc: KanidmClientConfig) -> Result<Self, ()> {
|
||||||
client: client,
|
let KanidmClientBuilder {
|
||||||
addr: addr.to_string(),
|
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,
|
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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue