diff --git a/Cargo.lock b/Cargo.lock index 3f1cfbd93..caee2b2c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,9 +82,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" [[package]] name = "anymap" @@ -273,7 +273,7 @@ dependencies = [ "async-io", "async-lock", "async-process", - "crossbeam-utils 0.8.3", + "crossbeam-utils", "futures-channel", "futures-core", "futures-io", @@ -512,11 +512,11 @@ checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" [[package]] name = "byte-pool" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38e98299d518ec351ca016363e0cbfc77059dcd08dfa9700d15e405536097a" +checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca" dependencies = [ - "crossbeam-queue 0.2.3", + "crossbeam-queue", "stable_deref_trait", ] @@ -634,7 +634,7 @@ dependencies = [ "ahash 0.7.2", "crossbeam", "crossbeam-epoch", - "crossbeam-utils 0.8.3", + "crossbeam-utils", "num", "packed_simd_2", "parking_lot", @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "const_fn" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" +checksum = "076a6803b0dacd6a88cfe64deba628b01533ff5ef265687e6938280c1afd0a28" [[package]] name = "constant_time_eq" @@ -780,8 +780,8 @@ dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", - "crossbeam-queue 0.3.1", - "crossbeam-utils 0.8.3", + "crossbeam-queue", + "crossbeam-utils", ] [[package]] @@ -791,7 +791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.3", + "crossbeam-utils", ] [[package]] @@ -802,7 +802,7 @@ checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", - "crossbeam-utils 0.8.3", + "crossbeam-utils", ] [[package]] @@ -812,23 +812,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.3", + "crossbeam-utils", "lazy_static", "memoffset", "scopeguard", ] -[[package]] -name = "crossbeam-queue" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" -dependencies = [ - "cfg-if 0.1.10", - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - [[package]] name = "crossbeam-queue" version = "0.3.1" @@ -836,18 +825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.3", -] - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "lazy_static", + "crossbeam-utils", ] [[package]] @@ -1414,9 +1392,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d832b01df74254fe364568d6ddc294443f61cbec82816b60904303af87efae78" +checksum = "fc018e188373e2777d0ef2467ebff62a08e66c3f5857b23c8fbec3018210dc00" dependencies = [ "bytes", "fnv", @@ -1571,9 +1549,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" +checksum = "8bf09f61b52cfcf4c00de50df88ae423d6c02354e385a86341133b5338630ad1" dependencies = [ "bytes", "futures-channel", @@ -1586,7 +1564,7 @@ dependencies = [ "httpdate", "itoa", "pin-project", - "socket2 0.3.19", + "socket2", "tokio", "tower-service", "tracing", @@ -1713,9 +1691,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821" +checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" dependencies = [ "wasm-bindgen", ] @@ -1923,9 +1901,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" +checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" [[package]] name = "libm" @@ -2023,12 +2001,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "memchr" version = "2.3.4" @@ -2037,9 +2009,9 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "memoffset" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" +checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" dependencies = [ "autocfg", ] @@ -2097,7 +2069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19900e7eee95eb2b3c2e26d12a874cc80aaf750e31be6fcbe743ead369fa45d" dependencies = [ "libc", - "socket2 0.4.0", + "socket2", ] [[package]] @@ -2384,18 +2356,18 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63" +checksum = "bc174859768806e91ae575187ada95c91a29e96a98dc5d2cd9a1fed039501ba6" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" +checksum = "a490329918e856ed1b083f244e3bfe2d8c4f336407e4ea9e1a9f479ff09049e5" dependencies = [ "proc-macro2", "quote", @@ -2691,7 +2663,7 @@ checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" dependencies = [ "crossbeam-channel", "crossbeam-deque", - "crossbeam-utils 0.8.3", + "crossbeam-utils", "lazy_static", "num_cpus", ] @@ -2915,9 +2887,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d" +checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84" dependencies = [ "bitflags", "core-foundation", @@ -2928,9 +2900,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d" +checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339" dependencies = [ "core-foundation-sys", "libc", @@ -3118,17 +3090,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "socket2" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.4.0" @@ -3270,9 +3231,9 @@ checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08" [[package]] name = "syn" -version = "1.0.64" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" +checksum = "6498a9efc342871f91cc2d0d694c674368b4ceb40f62b65a7a08c3792935e702" dependencies = [ "proc-macro2", "quote", @@ -3734,9 +3695,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" dependencies = [ "cfg-if 1.0.0", "serde", @@ -3746,9 +3707,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" dependencies = [ "bumpalo", "lazy_static", @@ -3761,9 +3722,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73157efb9af26fb564bb59a009afd1c7c334a44db171d280690d0c3faaec3468" +checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -3773,9 +3734,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3783,9 +3744,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" dependencies = [ "proc-macro2", "quote", @@ -3796,15 +3757,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" [[package]] name = "web-sys" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/kanidm_client/Cargo.toml b/kanidm_client/Cargo.toml index b1ec2841b..77d8b387e 100644 --- a/kanidm_client/Cargo.toml +++ b/kanidm_client/Cargo.toml @@ -21,6 +21,7 @@ toml = "0.5" uuid = { version = "0.8", features = ["serde", "v4"] } url = "2.1.1" webauthn-rs = "0.3.0-alpha.7" +tokio = { version = "1", features = ["rt", "net", "time", "macros", "sync", "signal"] } [dev-dependencies] tokio = { version = "1", features = ["rt", "net", "time", "macros", "sync", "signal"] } diff --git a/kanidm_client/src/asynchronous.rs b/kanidm_client/src/asynchronous.rs index 8a84c0b5e..d05afcd4c 100644 --- a/kanidm_client/src/asynchronous.rs +++ b/kanidm_client/src/asynchronous.rs @@ -1,8 +1,15 @@ -use crate::{ClientError, KanidmClientBuilder, APPLICATION_JSON, KOPID}; +use crate::{ClientError, KanidmClientBuilder, APPLICATION_JSON, KOPID, KSESSIONID}; use reqwest::header::CONTENT_TYPE; use serde::de::DeserializeOwned; use serde::Serialize; +use std::collections::BTreeMap; use std::collections::BTreeSet as Set; +use uuid::Uuid; + +use webauthn_rs::proto::{ + CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential, + RequestChallengeResponse, +}; use kanidm_proto::v1::*; @@ -10,11 +17,107 @@ use kanidm_proto::v1::*; pub struct KanidmAsyncClient { pub(crate) client: reqwest::Client, pub(crate) addr: String, + pub(crate) origin: String, pub(crate) builder: KanidmClientBuilder, pub(crate) bearer_token: Option, + pub(crate) auth_session_id: Option, } impl KanidmAsyncClient { + pub fn get_origin(&self) -> &str { + self.origin.as_str() + } + + pub fn set_token(&mut self, new_token: String) { + let mut new_token = Some(new_token); + std::mem::swap(&mut self.bearer_token, &mut new_token); + } + + pub fn get_token(&self) -> Option<&str> { + self.bearer_token.as_deref() + } + + pub fn new_session(&self) -> Result { + // Copy our builder, and then just process it. + let builder = self.builder.clone(); + builder.build_async() + } + + pub fn logout(&mut self) -> Result<(), reqwest::Error> { + // 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 KanidmAsyncClient { mut client, .. } = builder.build_async()?; + + std::mem::swap(&mut self.client, &mut client); + Ok(()) + } + + async fn perform_auth_post_request( + &mut self, + dest: &str, + request: R, + ) -> Result { + let dest = [self.addr.as_str(), dest].concat(); + debug!("{:?}", dest); + // format doesn't work in async ?! + // let dest = format!("{}{}", self.addr, dest); + + let req_string = serde_json::to_string(&request).map_err(ClientError::JSONEncode)?; + + let response = self + .client + .post(dest.as_str()) + .body(req_string) + .header(CONTENT_TYPE, APPLICATION_JSON); + let response = if let Some(token) = &self.bearer_token { + response.bearer_auth(token) + } else { + response + }; + + // If we have a session header, set it now. + let response = if let Some(sessionid) = &self.auth_session_id { + response.header(KSESSIONID, sessionid) + } else { + response + }; + + let response = response.send().await.map_err(ClientError::Transport)?; + + // If we have a sessionid header in the response, get it now. + + let headers = response.headers(); + + self.auth_session_id = headers + .get(KSESSIONID) + .map(|hv| hv.to_str().ok().map(|s| s.to_string())) + .flatten(); + + let opid = headers + .get(KOPID) + .and_then(|hv| hv.to_str().ok().map(|s| s.to_string())) + .unwrap_or_else(|| "missing_kopid".to_string()); + debug!("opid -> {:?}", opid); + + match response.status() { + reqwest::StatusCode::OK => {} + unexpect => { + return Err(ClientError::Http( + unexpect, + response.json().await.ok(), + opid, + )) + } + } + + response + .json() + .await + .map_err(|e| ClientError::JSONDecode(e, opid)) + } + async fn perform_post_request( &self, dest: &str, @@ -190,14 +293,16 @@ impl KanidmAsyncClient { .map_err(|e| ClientError::JSONDecode(e, opid)) } - pub async fn auth_step_init(&self, ident: &str) -> Result, ClientError> { + pub async fn auth_step_init(&mut self, ident: &str) -> Result, ClientError> { let auth_init = AuthRequest { step: AuthStep::Init(ident.to_string()), }; - let r: Result = self.perform_post_request("/v1/auth", auth_init).await; + let r: Result = + self.perform_auth_post_request("/v1/auth", auth_init).await; r.map(|v| { debug!("Authentication Session ID -> {:?}", v.sessionid); + // Stash the session ID header. v.state }) .and_then(|state| match state { @@ -207,12 +312,16 @@ impl KanidmAsyncClient { .map(|mechs| mechs.into_iter().collect()) } - pub async fn auth_step_begin(&self, mech: AuthMech) -> Result, ClientError> { + pub async fn auth_step_begin( + &mut self, + mech: AuthMech, + ) -> Result, ClientError> { let auth_begin = AuthRequest { step: AuthStep::Begin(mech), }; - let r: Result = self.perform_post_request("/v1/auth", auth_begin).await; + let r: Result = + self.perform_auth_post_request("/v1/auth", auth_begin).await; r.map(|v| { debug!("Authentication Session ID -> {:?}", v.sessionid); v.state @@ -225,6 +334,96 @@ impl KanidmAsyncClient { // .map(|allowed| allowed.into_iter().collect()) } + pub async fn auth_step_anonymous(&mut self) -> Result { + let auth_anon = AuthRequest { + step: AuthStep::Cred(AuthCredential::Anonymous), + }; + let r: Result = + self.perform_auth_post_request("/v1/auth", auth_anon).await; + + r.map(|ar| { + if let AuthState::Success(token) = &ar.state { + self.bearer_token = Some(token.clone()); + }; + ar + }) + } + + pub async fn auth_step_password( + &mut self, + password: &str, + ) -> Result { + let auth_req = AuthRequest { + step: AuthStep::Cred(AuthCredential::Password(password.to_string())), + }; + let r: Result = self.perform_auth_post_request("/v1/auth", auth_req).await; + + r.map(|ar| { + if let AuthState::Success(token) = &ar.state { + self.bearer_token = Some(token.clone()); + }; + ar + }) + } + + pub async fn auth_step_totp(&mut self, totp: u32) -> Result { + let auth_req = AuthRequest { + step: AuthStep::Cred(AuthCredential::TOTP(totp)), + }; + let r: Result = self.perform_auth_post_request("/v1/auth", auth_req).await; + + r.map(|ar| { + if let AuthState::Success(token) = &ar.state { + self.bearer_token = Some(token.clone()); + }; + ar + }) + } + + pub async fn auth_step_webauthn_complete( + &mut self, + pkc: PublicKeyCredential, + ) -> Result { + let auth_req = AuthRequest { + step: AuthStep::Cred(AuthCredential::Webauthn(pkc)), + }; + let r: Result = self.perform_auth_post_request("/v1/auth", auth_req).await; + + r.map(|ar| { + if let AuthState::Success(token) = &ar.state { + self.bearer_token = Some(token.clone()); + }; + ar + }) + } + + pub async fn auth_anonymous(&mut self) -> Result<(), ClientError> { + let mechs = match self.auth_step_init("anonymous").await { + Ok(s) => s, + Err(e) => return Err(e), + }; + + if !mechs.contains(&AuthMech::Anonymous) { + debug!("Anonymous mech not presented"); + return Err(ClientError::AuthenticationFailed); + } + + let _state = match self.auth_step_begin(AuthMech::Anonymous).await { + Ok(s) => s, + Err(e) => return Err(e), + }; + + let r = self.auth_step_anonymous().await?; + + match r.state { + AuthState::Success(token) => { + self.bearer_token = Some(token); + Ok(()) + } + _ => Err(ClientError::AuthenticationFailed), + } + } + pub async fn auth_simple_password( &mut self, ident: &str, @@ -245,50 +444,97 @@ impl KanidmAsyncClient { Err(e) => return Err(e), }; - let auth_req = AuthRequest { - step: AuthStep::Cred(AuthCredential::Password(password.to_string())), - }; - let r: Result = self.perform_post_request("/v1/auth", auth_req).await; - - let r = r?; + let r = self.auth_step_password(password).await?; match r.state { - AuthState::Success(token) => { - self.bearer_token = Some(token); - Ok(()) - } + AuthState::Success(_) => Ok(()), _ => Err(ClientError::AuthenticationFailed), } } - pub async fn auth_anonymous(&mut self) -> Result<(), ClientError> { - let mechs = match self.auth_step_init("anonymous").await { + pub async fn auth_password_totp( + &mut self, + ident: &str, + password: &str, + totp: u32, + ) -> Result<(), ClientError> { + let mechs = match self.auth_step_init(ident).await { Ok(s) => s, Err(e) => return Err(e), }; - if !mechs.contains(&AuthMech::Anonymous) { - debug!("Anonymous mech not presented"); + if !mechs.contains(&AuthMech::PasswordMFA) { + debug!("PasswordMFA mech not presented"); return Err(ClientError::AuthenticationFailed); } - let _state = match self.auth_step_begin(AuthMech::Anonymous).await { + let state = match self.auth_step_begin(AuthMech::PasswordMFA).await { Ok(s) => s, Err(e) => return Err(e), }; - let auth_anon = AuthRequest { - step: AuthStep::Cred(AuthCredential::Anonymous), - }; - let r: Result = self.perform_post_request("/v1/auth", auth_anon).await; + if !state.contains(&AuthAllowed::TOTP) { + debug!("TOTP step not offered."); + return Err(ClientError::AuthenticationFailed); + } - let r = r?; + let r = self.auth_step_totp(totp).await?; + + // Should need to continue. + match r.state { + AuthState::Continue(allowed) => { + if !allowed.contains(&AuthAllowed::Password) { + debug!("Password step not offered."); + return Err(ClientError::AuthenticationFailed); + } + } + _ => { + debug!("Invalid AuthState presented."); + return Err(ClientError::AuthenticationFailed); + } + }; + + let r = self.auth_step_password(password).await?; match r.state { - AuthState::Success(token) => { - self.bearer_token = Some(token); - Ok(()) - } + AuthState::Success(_token) => Ok(()), + _ => Err(ClientError::AuthenticationFailed), + } + } + + pub async fn auth_webauthn_begin( + &mut self, + ident: &str, + ) -> Result { + let mechs = match self.auth_step_init(ident).await { + Ok(s) => s, + Err(e) => return Err(e), + }; + + if !mechs.contains(&AuthMech::Webauthn) { + debug!("Webauthn mech not presented"); + return Err(ClientError::AuthenticationFailed); + } + + let mut state = match self.auth_step_begin(AuthMech::Webauthn).await { + Ok(s) => s, + Err(e) => return Err(e), + }; + + // State is now a set of auth continues. + match state.pop() { + Some(AuthAllowed::Webauthn(r)) => Ok(r), + _ => Err(ClientError::AuthenticationFailed), + } + } + + pub async fn auth_webauthn_complete( + &mut self, + pkc: PublicKeyCredential, + ) -> Result<(), ClientError> { + let r = self.auth_step_webauthn_complete(pkc).await?; + match r.state { + AuthState::Success(_token) => Ok(()), _ => Err(ClientError::AuthenticationFailed), } } @@ -297,12 +543,15 @@ impl KanidmAsyncClient { let whoami_dest = [self.addr.as_str(), "/v1/self"].concat(); // format!("{}/v1/self", self.addr); debug!("{:?}", whoami_dest); - let response = self - .client - .get(whoami_dest.as_str()) - .send() - .await - .map_err(ClientError::Transport)?; + let response = self.client.get(whoami_dest.as_str()); + + let response = if let Some(token) = &self.bearer_token { + response.bearer_auth(token) + } else { + response + }; + + let response = response.send().await.map_err(ClientError::Transport)?; let opid = response .headers() @@ -332,6 +581,170 @@ impl KanidmAsyncClient { Ok(Some((r.youare, r.uat))) } + // Raw DB actions + pub async fn search(&self, filter: Filter) -> Result, ClientError> { + let sr = SearchRequest { filter }; + let r: Result = self.perform_post_request("/v1/raw/search", sr).await; + r.map(|v| v.entries) + } + + pub async fn create(&self, entries: Vec) -> Result { + let c = CreateRequest { entries }; + let r: Result = self.perform_post_request("/v1/raw/create", c).await; + r.map(|_| true) + } + + pub async fn modify(&self, filter: Filter, modlist: ModifyList) -> Result { + let mr = ModifyRequest { filter, modlist }; + let r: Result = self.perform_post_request("/v1/raw/modify", mr).await; + r.map(|_| true) + } + + pub async fn delete(&self, filter: Filter) -> Result { + let dr = DeleteRequest { filter }; + let r: Result = self.perform_post_request("/v1/raw/delete", dr).await; + r.map(|_| true) + } + + // === idm actions here == + + // ===== GROUPS + pub async fn idm_group_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/group").await + } + + pub async fn idm_group_get(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(format!("/v1/group/{}", id).as_str()) + .await + } + + pub async fn idm_group_get_members( + &self, + id: &str, + ) -> Result>, ClientError> { + self.perform_get_request(format!("/v1/group/{}/_attr/member", id).as_str()) + .await + } + + pub async fn idm_group_create(&self, name: &str) -> Result { + let mut new_group = Entry { + attrs: BTreeMap::new(), + }; + new_group + .attrs + .insert("name".to_string(), vec![name.to_string()]); + self.perform_post_request("/v1/group", new_group) + .await + .map(|_: OperationResponse| true) + } + + pub async fn idm_group_set_members( + &self, + id: &str, + members: &[&str], + ) -> Result { + let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect(); + self.perform_put_request(format!("/v1/group/{}/_attr/member", id).as_str(), m) + .await + } + + pub async fn idm_group_add_members( + &self, + id: &str, + members: &[&str], + ) -> Result { + let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect(); + self.perform_post_request(["/v1/group/", id, "/_attr/member"].concat().as_str(), m) + .await + } + + /* + pub fn idm_group_remove_member(&self, id: &str, member: &str) -> Result<(), ClientError> { + unimplemented!(); + } + */ + + pub async fn idm_group_purge_members(&self, id: &str) -> Result { + self.perform_delete_request(format!("/v1/group/{}/_attr/member", id).as_str()) + .await + } + + pub async fn idm_group_unix_extend( + &self, + id: &str, + gidnumber: Option, + ) -> Result { + let gx = GroupUnixExtend { gidnumber }; + self.perform_post_request(format!("/v1/group/{}/_unix", id).as_str(), gx) + .await + } + + pub async fn idm_group_unix_token_get(&self, id: &str) -> Result { + // Format doesn't work in async + // format!("/v1/account/{}/_unix/_token", id).as_str() + self.perform_get_request(["/v1/group/", id, "/_unix/_token"].concat().as_str()) + .await + } + + pub async fn idm_group_delete(&self, id: &str) -> Result { + self.perform_delete_request(["/v1/group/", id].concat().as_str()) + .await + } + + // ==== ACCOUNTS + pub async fn idm_account_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/account").await + } + + pub async fn idm_account_create(&self, name: &str, dn: &str) -> Result { + let mut new_acct = Entry { + attrs: BTreeMap::new(), + }; + new_acct + .attrs + .insert("name".to_string(), vec![name.to_string()]); + new_acct + .attrs + .insert("displayname".to_string(), vec![dn.to_string()]); + self.perform_post_request("/v1/account", new_acct) + .await + .map(|_: OperationResponse| true) + } + + pub async fn idm_account_set_password(&self, cleartext: String) -> Result { + let s = SingleStringRequest { value: cleartext }; + + let r: Result = self + .perform_post_request("/v1/self/_credential/primary/set_password", s) + .await; + r.map(|_| true) + } + + pub async fn idm_account_set_displayname( + &self, + id: &str, + dn: &str, + ) -> Result { + self.idm_account_set_attr(id, "displayname", &[dn]).await + } + + pub async fn idm_account_unix_token_get(&self, id: &str) -> Result { + // Format doesn't work in async + // format!("/v1/account/{}/_unix/_token", id).as_str() + self.perform_get_request(["/v1/account/", id, "/_unix/_token"].concat().as_str()) + .await + } + + pub async fn idm_account_delete(&self, id: &str) -> Result { + self.perform_delete_request(["/v1/account/", id].concat().as_str()) + .await + } + + pub async fn idm_account_get(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(format!("/v1/account/{}", id).as_str()) + .await + } + pub async fn idm_account_set_attr( &self, id: &str, @@ -343,27 +756,240 @@ impl KanidmAsyncClient { .await } - pub async fn idm_account_unix_token_get(&self, id: &str) -> Result { - // Format doesn't work in async - // format!("/v1/account/{}/_unix/_token", id).as_str() - self.perform_get_request(["/v1/account/", id, "/_unix/_token"].concat().as_str()) + pub async fn idm_account_get_attr( + &self, + id: &str, + attr: &str, + ) -> Result>, ClientError> { + self.perform_get_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str()) .await } - pub async fn idm_group_unix_token_get(&self, id: &str) -> Result { - // Format doesn't work in async - // format!("/v1/account/{}/_unix/_token", id).as_str() - self.perform_get_request(["/v1/group/", id, "/_unix/_token"].concat().as_str()) + pub async fn idm_account_purge_attr(&self, id: &str, attr: &str) -> Result { + self.perform_delete_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str()) .await } - pub async fn idm_account_delete(&self, id: &str) -> Result { - self.perform_delete_request(["/v1/account/", id].concat().as_str()) + pub async fn idm_account_primary_credential_set_password( + &self, + id: &str, + pw: &str, + ) -> Result { + let r = SetCredentialRequest::Password(pw.to_string()); + self.perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await + } + + pub async fn idm_account_primary_credential_import_password( + &self, + id: &str, + pw: &str, + ) -> Result { + self.perform_put_request( + format!("/v1/account/{}/_attr/password_import", id).as_str(), + vec![pw.to_string()], + ) + .await + } + + pub async fn idm_account_primary_credential_set_generated( + &self, + id: &str, + ) -> Result { + let r = SetCredentialRequest::GeneratePassword; + let res: Result = self + .perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await; + match res { + Ok(SetCredentialResponse::Token(p)) => Ok(p), + Ok(_) => Err(ClientError::EmptyResponse), + Err(e) => Err(e), + } + } + + // Reg intent for totp + pub async fn idm_account_primary_credential_generate_totp( + &self, + id: &str, + label: &str, + ) -> Result<(Uuid, TOTPSecret), ClientError> { + let r = SetCredentialRequest::TOTPGenerate(label.to_string()); + let res: Result = self + .perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await; + match res { + Ok(SetCredentialResponse::TOTPCheck(u, s)) => Ok((u, s)), + Ok(_) => Err(ClientError::EmptyResponse), + Err(e) => Err(e), + } + } + + // Verify the totp + pub async fn idm_account_primary_credential_verify_totp( + &self, + id: &str, + otp: u32, + session: Uuid, + ) -> Result { + let r = SetCredentialRequest::TOTPVerify(session, otp); + let res: Result = self + .perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await; + match res { + Ok(SetCredentialResponse::Success) => Ok(true), + Ok(SetCredentialResponse::TOTPCheck(u, s)) => Err(ClientError::TOTPVerifyFailed(u, s)), + Ok(_) => Err(ClientError::EmptyResponse), + Err(e) => Err(e), + } + } + + pub async fn idm_account_primary_credential_remove_totp( + &self, + id: &str, + ) -> Result { + let r = SetCredentialRequest::TOTPRemove; + let res: Result = self + .perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await; + match res { + Ok(SetCredentialResponse::Success) => Ok(true), + Ok(_) => Err(ClientError::EmptyResponse), + Err(e) => Err(e), + } + } + + pub async fn idm_account_primary_credential_register_webauthn( + &self, + id: &str, + label: &str, + ) -> Result<(Uuid, CreationChallengeResponse), ClientError> { + let r = SetCredentialRequest::WebauthnBegin(label.to_string()); + let res: Result = self + .perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await; + match res { + Ok(SetCredentialResponse::WebauthnCreateChallenge(u, s)) => Ok((u, s)), + Ok(_) => Err(ClientError::EmptyResponse), + Err(e) => Err(e), + } + } + + pub async fn idm_account_primary_credential_complete_webuthn_registration( + &self, + id: &str, + rego: RegisterPublicKeyCredential, + session: Uuid, + ) -> Result<(), ClientError> { + let r = SetCredentialRequest::WebauthnRegister(session, rego); + let res: Result = self + .perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await; + match res { + Ok(SetCredentialResponse::Success) => Ok(()), + Ok(_) => Err(ClientError::EmptyResponse), + Err(e) => Err(e), + } + } + + pub async fn idm_account_primary_credential_remove_webauthn( + &self, + id: &str, + label: &str, + ) -> Result { + let r = SetCredentialRequest::WebauthnRemove(label.to_string()); + let res: Result = self + .perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await; + match res { + Ok(SetCredentialResponse::Success) => Ok(true), + Ok(_) => Err(ClientError::EmptyResponse), + Err(e) => Err(e), + } + } + + pub async fn idm_account_get_credential_status( + &self, + id: &str, + ) -> Result { + let res: Result = self + .perform_get_request(format!("/v1/account/{}/_credential/_status", id).as_str()) + .await; + res.and_then(|cs| { + if cs.creds.is_empty() { + Err(ClientError::EmptyResponse) + } else { + Ok(cs) + } + }) + } + + pub async fn idm_account_radius_credential_get( + &self, + id: &str, + ) -> Result, ClientError> { + self.perform_get_request(format!("/v1/account/{}/_radius", id).as_str()) .await } - pub async fn idm_group_delete(&self, id: &str) -> Result { - self.perform_delete_request(["/v1/group/", id].concat().as_str()) + pub async fn idm_account_radius_credential_regenerate( + &self, + id: &str, + ) -> Result { + self.perform_post_request(format!("/v1/account/{}/_radius", id).as_str(), ()) + .await + } + + pub async fn idm_account_radius_credential_delete( + &self, + id: &str, + ) -> Result { + self.perform_delete_request(format!("/v1/account/{}/_radius", id).as_str()) + .await + } + + pub async fn idm_account_radius_token_get( + &self, + id: &str, + ) -> Result { + self.perform_get_request(format!("/v1/account/{}/_radius/_token", id).as_str()) + .await + } + + pub async fn idm_account_unix_extend( + &self, + id: &str, + gidnumber: Option, + shell: Option<&str>, + ) -> Result { + let ux = AccountUnixExtend { + shell: shell.map(|s| s.to_string()), + gidnumber, + }; + self.perform_post_request(format!("/v1/account/{}/_unix", id).as_str(), ux) .await } @@ -399,13 +1025,114 @@ impl KanidmAsyncClient { .await } - pub async fn idm_group_add_members( + pub async fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str()) + .await + } + + pub async fn idm_account_post_ssh_pubkey( &self, id: &str, - members: Vec<&str>, + tag: &str, + pubkey: &str, ) -> Result { - let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect(); - self.perform_post_request(["/v1/group/", id, "/_attr/member"].concat().as_str(), m) + let sk = (tag.to_string(), pubkey.to_string()); + self.perform_post_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str(), sk) + .await + } + + pub async fn idm_account_person_extend(&self, id: &str) -> Result { + self.perform_post_request(format!("/v1/account/{}/_person/_extend", id).as_str(), ()) + .await + } + + pub async fn idm_account_get_ssh_pubkey( + &self, + id: &str, + tag: &str, + ) -> Result, ClientError> { + self.perform_get_request(format!("/v1/account/{}/_ssh_pubkeys/{}", id, tag).as_str()) + .await + } + + pub async fn idm_account_delete_ssh_pubkey( + &self, + id: &str, + tag: &str, + ) -> Result { + self.perform_delete_request(format!("/v1/account/{}/_ssh_pubkeys/{}", id, tag).as_str()) + .await + } + + // ==== domain_info (aka domain) + pub async fn idm_domain_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/domain").await + } + + pub async fn idm_domain_get(&self, id: &str) -> Result { + self.perform_get_request(format!("/v1/domain/{}", id).as_str()) + .await + } + + // pub fn idm_domain_get_attr + pub async fn idm_domain_get_ssid(&self, id: &str) -> Result { + self.perform_get_request(format!("/v1/domain/{}/_attr/domain_ssid", id).as_str()) + .await + .and_then(|mut r: Vec| + // Get the first result + r.pop() + .ok_or( + ClientError::EmptyResponse + )) + } + + // pub fn idm_domain_put_attr + pub async fn idm_domain_set_ssid(&self, id: &str, ssid: &str) -> Result { + self.perform_put_request( + format!("/v1/domain/{}/_attr/domain_ssid", id).as_str(), + vec![ssid.to_string()], + ) + .await + } + + // ==== schema + pub async fn idm_schema_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/schema").await + } + + pub async fn idm_schema_attributetype_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/schema/attributetype").await + } + + pub async fn idm_schema_attributetype_get( + &self, + id: &str, + ) -> Result, ClientError> { + self.perform_get_request(format!("/v1/schema/attributetype/{}", id).as_str()) + .await + } + + pub async fn idm_schema_classtype_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/schema/classtype").await + } + + pub async fn idm_schema_classtype_get(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(format!("/v1/schema/classtype/{}", id).as_str()) + .await + } + + // ==== recycle bin + pub async fn recycle_bin_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/recycle_bin").await + } + + pub async fn recycle_bin_get(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(format!("/v1/recycle_bin/{}", id).as_str()) + .await + } + + pub async fn recycle_bin_revive(&self, id: &str) -> Result { + self.perform_post_request(format!("/v1/recycle_bin/{}/_revive", id).as_str(), ()) .await } } diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index cebc50b65..d276901eb 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -11,12 +11,8 @@ #[macro_use] extern crate log; -use reqwest::header::CONTENT_TYPE; -use serde::de::DeserializeOwned; -use serde::Serialize; use serde_derive::Deserialize; use serde_json::error::Error as SerdeJsonError; -use std::collections::BTreeMap; use std::collections::BTreeSet as Set; use std::fs::{metadata, File, Metadata}; use std::io::Read; @@ -32,13 +28,7 @@ use webauthn_rs::proto::{ }; // use users::{get_current_uid, get_effective_uid}; -use kanidm_proto::v1::{ - AccountUnixExtend, AuthAllowed, AuthCredential, AuthMech, AuthRequest, AuthResponse, AuthState, - AuthStep, CreateRequest, CredentialStatus, DeleteRequest, Entry, Filter, GroupUnixExtend, - ModifyList, ModifyRequest, OperationError, OperationResponse, RadiusAuthToken, SearchRequest, - SearchResponse, SetCredentialRequest, SetCredentialResponse, SingleStringRequest, TOTPSecret, - UnixGroupToken, UnixUserToken, UserAuthToken, WhoamiResponse, -}; +use kanidm_proto::v1::*; pub mod asynchronous; @@ -46,6 +36,7 @@ use crate::asynchronous::KanidmAsyncClient; pub const APPLICATION_JSON: &str = "application/json"; pub const KOPID: &str = "X-KANIDM-OPID"; +pub const KSESSIONID: &str = "X-KANIDM-AUTH-SESSION-ID"; #[derive(Debug)] pub enum ClientError { @@ -251,6 +242,10 @@ impl KanidmClientBuilder { // Consume self and return a client. pub fn build(self) -> Result { + self.build_async().map(|asclient| KanidmClient { asclient }) + } + + pub fn build_async(self) -> Result { // Errghh, how to handle this cleaner. let address = match &self.address { Some(a) => a.clone(), @@ -262,8 +257,7 @@ impl KanidmClientBuilder { self.display_warnings(address.as_str()); - let client_builder = reqwest::blocking::Client::builder() - .cookie_store(true) + let client_builder = reqwest::Client::builder() .danger_accept_invalid_hostnames(!self.verify_hostnames) .danger_accept_invalid_certs(!self.verify_ca); @@ -291,378 +285,98 @@ impl KanidmClientBuilder { .map(|h| format!("{}://{}", uri.scheme(), h)) .expect("can not fail"); - Ok(KanidmClient { - client, - addr: address, - origin, - builder: self, - bearer_token: None, - }) - } - - pub fn build_async(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!(); - } - }; - - self.display_warnings(address.as_str()); - - let client_builder = reqwest::Client::builder() - .cookie_store(true) - .danger_accept_invalid_hostnames(!self.verify_hostnames) - .danger_accept_invalid_certs(!self.verify_ca); - - let client_builder = match &self.ca { - Some(cert) => client_builder.add_root_certificate(cert.clone()), - None => client_builder, - }; - - let client_builder = match &self.connect_timeout { - Some(secs) => client_builder - .connect_timeout(Duration::from_secs(*secs)) - .timeout(Duration::from_secs(*secs)), - None => client_builder, - }; - - let client = client_builder.build()?; - Ok(KanidmAsyncClient { client, addr: address, builder: self, bearer_token: None, + origin, + auth_session_id: None, }) } } #[derive(Debug)] pub struct KanidmClient { - client: reqwest::blocking::Client, - addr: String, - origin: String, - builder: KanidmClientBuilder, - bearer_token: Option, + asclient: KanidmAsyncClient, +} + +#[allow(clippy::expect_used)] +fn tokio_block_on(f: F) -> R +where + F: std::future::Future + std::future::Future, +{ + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to start tokio"); + rt.block_on(f) } impl KanidmClient { pub fn get_origin(&self) -> &str { - self.origin.as_str() + self.asclient.get_origin() } pub fn new_session(&self) -> Result { // Copy our builder, and then just process it. - let builder = self.builder.clone(); - builder.build() + self.asclient + .new_session() + .map(|asclient| KanidmClient { asclient }) } pub fn set_token(&mut self, new_token: String) { - let mut new_token = Some(new_token); - std::mem::swap(&mut self.bearer_token, &mut new_token); + self.asclient.set_token(new_token); } pub fn get_token(&self) -> Option<&str> { - self.bearer_token.as_deref() + self.asclient.get_token() } pub fn logout(&mut self) -> Result<(), reqwest::Error> { - // 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, .. } = builder.build()?; - - std::mem::swap(&mut self.client, &mut client); - Ok(()) - } - - fn perform_post_request( - &self, - dest: &str, - request: R, - ) -> Result { - let dest = format!("{}{}", self.addr, dest); - - let req_string = serde_json::to_string(&request).map_err(ClientError::JSONEncode)?; - - let response = self - .client - .post(dest.as_str()) - .header(CONTENT_TYPE, APPLICATION_JSON); - - let response = if let Some(token) = &self.bearer_token { - response.bearer_auth(token) - } else { - response - }; - - let response = response - .body(req_string) - .send() - .map_err(ClientError::Transport)?; - - let opid = response - .headers() - .get(KOPID) - .and_then(|hv| hv.to_str().ok().map(|s| s.to_string())) - .unwrap_or_else(|| "missing_kopid".to_string()); - debug!("opid -> {:?}", opid); - - match response.status() { - reqwest::StatusCode::OK => {} - unexpect => return Err(ClientError::Http(unexpect, response.json().ok(), opid)), - } - - response - .json() - .map_err(|e| ClientError::JSONDecode(e, opid)) - } - - fn perform_put_request( - &self, - dest: &str, - request: R, - ) -> Result { - let dest = format!("{}{}", self.addr, dest); - - let req_string = serde_json::to_string(&request).map_err(ClientError::JSONEncode)?; - - let response = self - .client - .put(dest.as_str()) - .header(CONTENT_TYPE, APPLICATION_JSON); - - let response = if let Some(token) = &self.bearer_token { - response.bearer_auth(token) - } else { - response - }; - - let response = response - .body(req_string) - .send() - .map_err(ClientError::Transport)?; - - let opid = response - .headers() - .get(KOPID) - .and_then(|hv| hv.to_str().ok().map(|s| s.to_string())) - .unwrap_or_else(|| "missing_kopid".to_string()); - debug!("opid -> {:?}", opid); - - match response.status() { - reqwest::StatusCode::OK => {} - unexpect => return Err(ClientError::Http(unexpect, response.json().ok(), opid)), - } - - response - .json() - .map_err(|e| ClientError::JSONDecode(e, opid)) - } - - fn perform_get_request(&self, dest: &str) -> Result { - let dest = format!("{}{}", self.addr, dest); - let response = self.client.get(dest.as_str()); - - let response = if let Some(token) = &self.bearer_token { - response.bearer_auth(token) - } else { - response - }; - - let response = response.send().map_err(ClientError::Transport)?; - - let opid = response - .headers() - .get(KOPID) - .and_then(|hv| hv.to_str().ok().map(|s| s.to_string())) - .unwrap_or_else(|| "missing_kopid".to_string()); - debug!("opid -> {:?}", opid); - - match response.status() { - reqwest::StatusCode::OK => {} - unexpect => return Err(ClientError::Http(unexpect, response.json().ok(), opid)), - } - - response - .json() - .map_err(|e| ClientError::JSONDecode(e, opid)) - } - - fn perform_delete_request(&self, dest: &str) -> Result { - let dest = format!("{}{}", self.addr, dest); - let response = self.client.delete(dest.as_str()); - let response = if let Some(token) = &self.bearer_token { - response.bearer_auth(token) - } else { - response - }; - - let response = response.send().map_err(ClientError::Transport)?; - - let opid = response - .headers() - .get(KOPID) - .and_then(|hv| hv.to_str().ok().map(|s| s.to_string())) - .unwrap_or_else(|| "missing_kopid".to_string()); - debug!("opid -> {:?}", opid); - - match response.status() { - reqwest::StatusCode::OK => {} - unexpect => return Err(ClientError::Http(unexpect, response.json().ok(), opid)), - } - - response - .json() - .map_err(|e| ClientError::JSONDecode(e, opid)) + self.asclient.logout() } // whoami // Can't use generic get due to possible un-auth case. pub fn whoami(&self) -> Result, ClientError> { - let whoami_dest = format!("{}/v1/self", self.addr); - let response = self.client.get(whoami_dest.as_str()); - - let response = if let Some(token) = &self.bearer_token { - response.bearer_auth(token) - } else { - response - }; - - let response = response.send().map_err(ClientError::Transport)?; - - let opid = response - .headers() - .get(KOPID) - .and_then(|hv| hv.to_str().ok().map(|s| s.to_string())) - .unwrap_or_else(|| "missing_kopid".to_string()); - debug!("opid -> {:?}", opid); - - match response.status() { - // Continue to process. - reqwest::StatusCode::OK => {} - reqwest::StatusCode::UNAUTHORIZED => return Ok(None), - unexpect => return Err(ClientError::Http(unexpect, response.json().ok(), opid)), - } - - let r: WhoamiResponse = response - .json() - .map_err(|e| ClientError::JSONDecode(e, opid))?; - - Ok(Some((r.youare, r.uat))) + tokio_block_on(self.asclient.whoami()) } // auth - pub fn auth_step_anonymous(&mut self) -> Result { - let auth_anon = AuthRequest { - step: AuthStep::Cred(AuthCredential::Anonymous), - }; - let r: Result = self.perform_post_request("/v1/auth", auth_anon); + pub fn auth_step_init(&mut self, ident: &str) -> Result, ClientError> { + tokio_block_on(self.asclient.auth_step_init(ident)) + } - r.map(|ar| { - if let AuthState::Success(token) = &ar.state { - self.bearer_token = Some(token.clone()); - }; - ar - }) + pub fn auth_step_begin(&mut self, mech: AuthMech) -> Result, ClientError> { + tokio_block_on(self.asclient.auth_step_begin(mech)) + } + + pub fn auth_step_anonymous(&mut self) -> Result { + tokio_block_on(self.asclient.auth_step_anonymous()) } pub fn auth_step_password(&mut self, password: &str) -> Result { - let auth_req = AuthRequest { - step: AuthStep::Cred(AuthCredential::Password(password.to_string())), - }; - let r: Result = self.perform_post_request("/v1/auth", auth_req); - - r.map(|ar| { - if let AuthState::Success(token) = &ar.state { - self.bearer_token = Some(token.clone()); - }; - ar - }) + tokio_block_on(self.asclient.auth_step_password(password)) } pub fn auth_step_totp(&mut self, totp: u32) -> Result { - let auth_req = AuthRequest { - step: AuthStep::Cred(AuthCredential::TOTP(totp)), - }; - let r: Result = self.perform_post_request("/v1/auth", auth_req); - - r.map(|ar| { - if let AuthState::Success(token) = &ar.state { - self.bearer_token = Some(token.clone()); - }; - ar - }) + tokio_block_on(self.asclient.auth_step_totp(totp)) } pub fn auth_step_webauthn_complete( &mut self, pkc: PublicKeyCredential, ) -> Result { - let auth_req = AuthRequest { - step: AuthStep::Cred(AuthCredential::Webauthn(pkc)), - }; - let r: Result = self.perform_post_request("/v1/auth", auth_req); - - r.map(|ar| { - if let AuthState::Success(token) = &ar.state { - self.bearer_token = Some(token.clone()); - }; - ar - }) + tokio_block_on(self.asclient.auth_step_webauthn_complete(pkc)) } pub fn auth_anonymous(&mut self) -> Result<(), ClientError> { - let mechs = match self.auth_step_init("anonymous") { - Ok(s) => s, - Err(e) => return Err(e), - }; - - if !mechs.contains(&AuthMech::Anonymous) { - debug!("Anonymous mech not presented"); - return Err(ClientError::AuthenticationFailed); - } - - let _state = match self.auth_step_begin(AuthMech::Anonymous) { - Ok(s) => s, - Err(e) => return Err(e), - }; - - let r = self.auth_step_anonymous()?; - - match r.state { - AuthState::Success(_token) => Ok(()), - _ => Err(ClientError::AuthenticationFailed), - } + tokio_block_on(self.asclient.auth_anonymous()) } pub fn auth_simple_password(&mut self, ident: &str, password: &str) -> Result<(), ClientError> { - let mechs = match self.auth_step_init(ident) { - Ok(s) => s, - Err(e) => return Err(e), - }; - - if !mechs.contains(&AuthMech::Password) { - debug!("Password mech not presented"); - return Err(ClientError::AuthenticationFailed); - } - - let _state = match self.auth_step_begin(AuthMech::Password) { - Ok(s) => s, - Err(e) => return Err(e), - }; - - let r = self.auth_step_password(password)?; - - match r.state { - AuthState::Success(_token) => Ok(()), - _ => Err(ClientError::AuthenticationFailed), - } + tokio_block_on(self.asclient.auth_simple_password(ident, password)) } pub fn auth_password_totp( @@ -671,177 +385,61 @@ impl KanidmClient { password: &str, totp: u32, ) -> Result<(), ClientError> { - let mechs = match self.auth_step_init(ident) { - Ok(s) => s, - Err(e) => return Err(e), - }; - - if !mechs.contains(&AuthMech::PasswordMFA) { - debug!("PasswordMFA mech not presented"); - return Err(ClientError::AuthenticationFailed); - } - - let state = match self.auth_step_begin(AuthMech::PasswordMFA) { - Ok(s) => s, - Err(e) => return Err(e), - }; - - if !state.contains(&AuthAllowed::TOTP) { - debug!("TOTP step not offered."); - return Err(ClientError::AuthenticationFailed); - } - - let r = self.auth_step_totp(totp)?; - - // Should need to continue. - match r.state { - AuthState::Continue(allowed) => { - if !allowed.contains(&AuthAllowed::Password) { - debug!("Password step not offered."); - return Err(ClientError::AuthenticationFailed); - } - } - _ => { - debug!("Invalid AuthState presented."); - return Err(ClientError::AuthenticationFailed); - } - }; - - let r = self.auth_step_password(password)?; - - match r.state { - AuthState::Success(_token) => Ok(()), - _ => Err(ClientError::AuthenticationFailed), - } + tokio_block_on(self.asclient.auth_password_totp(ident, password, totp)) } pub fn auth_webauthn_begin( &mut self, ident: &str, ) -> Result { - let mechs = match self.auth_step_init(ident) { - Ok(s) => s, - Err(e) => return Err(e), - }; - - if !mechs.contains(&AuthMech::Webauthn) { - debug!("Webauthn mech not presented"); - return Err(ClientError::AuthenticationFailed); - } - - let mut state = match self.auth_step_begin(AuthMech::Webauthn) { - Ok(s) => s, - Err(e) => return Err(e), - }; - - // State is now a set of auth continues. - match state.pop() { - Some(AuthAllowed::Webauthn(r)) => Ok(r), - _ => Err(ClientError::AuthenticationFailed), - } + tokio_block_on(self.asclient.auth_webauthn_begin(ident)) } pub fn auth_webauthn_complete(&mut self, pkc: PublicKeyCredential) -> Result<(), ClientError> { - let r = self.auth_step_webauthn_complete(pkc)?; - match r.state { - AuthState::Success(_token) => Ok(()), - _ => Err(ClientError::AuthenticationFailed), - } + tokio_block_on(self.asclient.auth_webauthn_complete(pkc)) } // search pub fn search(&self, filter: Filter) -> Result, ClientError> { - let sr = SearchRequest { filter }; - let r: Result = self.perform_post_request("/v1/raw/search", sr); - r.map(|v| v.entries) + tokio_block_on(self.asclient.search(filter)) } // create pub fn create(&self, entries: Vec) -> Result { - let c = CreateRequest { entries }; - let r: Result = self.perform_post_request("/v1/raw/create", c); - r.map(|_| true) + tokio_block_on(self.asclient.create(entries)) } // modify pub fn modify(&self, filter: Filter, modlist: ModifyList) -> Result { - let mr = ModifyRequest { filter, modlist }; - let r: Result = self.perform_post_request("/v1/raw/modify", mr); - r.map(|_| true) + tokio_block_on(self.asclient.modify(filter, modlist)) } // delete pub fn delete(&self, filter: Filter) -> Result { - let dr = DeleteRequest { filter }; - let r: Result = self.perform_post_request("/v1/raw/delete", dr); - r.map(|_| true) + tokio_block_on(self.asclient.delete(filter)) } // === idm actions here == - pub fn idm_account_set_password(&self, cleartext: String) -> Result { - let s = SingleStringRequest { value: cleartext }; - - let r: Result = - self.perform_post_request("/v1/self/_credential/primary/set_password", s); - r.map(|_| true) - } - - pub fn auth_step_init(&self, ident: &str) -> Result, ClientError> { - let auth_init = AuthRequest { - step: AuthStep::Init(ident.to_string()), - }; - - let r: Result = self.perform_post_request("/v1/auth", auth_init); - r.map(|v| { - debug!("Authentication Session ID -> {:?}", v.sessionid); - v.state - }) - .and_then(|state| match state { - AuthState::Choose(mechs) => Ok(mechs), - _ => Err(ClientError::AuthenticationFailed), - }) - .map(|mechs| mechs.into_iter().collect()) - } - - pub fn auth_step_begin(&self, mech: AuthMech) -> Result, ClientError> { - let auth_begin = AuthRequest { - step: AuthStep::Begin(mech), - }; - - let r: Result = self.perform_post_request("/v1/auth", auth_begin); - r.map(|v| { - debug!("Authentication Session ID -> {:?}", v.sessionid); - v.state - }) - .and_then(|state| match state { - AuthState::Continue(allowed) => Ok(allowed), - _ => Err(ClientError::AuthenticationFailed), - }) - // For converting to a Set - // .map(|allowed| allowed.into_iter().collect()) - } // ===== GROUPS pub fn idm_group_list(&self) -> Result, ClientError> { - self.perform_get_request("/v1/group") + tokio_block_on(self.asclient.idm_group_list()) } pub fn idm_group_get(&self, id: &str) -> Result, ClientError> { - self.perform_get_request(format!("/v1/group/{}", id).as_str()) + tokio_block_on(self.asclient.idm_group_get(id)) } pub fn idm_group_get_members(&self, id: &str) -> Result>, ClientError> { - self.perform_get_request(format!("/v1/group/{}/_attr/member", id).as_str()) + tokio_block_on(self.asclient.idm_group_get_members(id)) } pub fn idm_group_set_members(&self, id: &str, members: &[&str]) -> Result { - let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect(); - self.perform_put_request(format!("/v1/group/{}/_attr/member", id).as_str(), m) + tokio_block_on(self.asclient.idm_group_set_members(id, members)) } pub fn idm_group_add_members(&self, id: &str, members: &[&str]) -> Result { - let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect(); - self.perform_post_request(format!("/v1/group/{}/_attr/member", id).as_str(), m) + tokio_block_on(self.asclient.idm_group_add_members(id, members)) } /* @@ -851,11 +449,11 @@ impl KanidmClient { */ pub fn idm_group_purge_members(&self, id: &str) -> Result { - self.perform_delete_request(format!("/v1/group/{}/_attr/member", id).as_str()) + tokio_block_on(self.asclient.idm_group_purge_members(id)) } pub fn idm_group_unix_token_get(&self, id: &str) -> Result { - self.perform_get_request(format!("/v1/group/{}/_unix/_token", id).as_str()) + tokio_block_on(self.asclient.idm_group_unix_token_get(id)) } pub fn idm_group_unix_extend( @@ -863,57 +461,48 @@ impl KanidmClient { id: &str, gidnumber: Option, ) -> Result { - let gx = GroupUnixExtend { gidnumber }; - self.perform_post_request(format!("/v1/group/{}/_unix", id).as_str(), gx) + tokio_block_on(self.asclient.idm_group_unix_extend(id, gidnumber)) } pub fn idm_group_delete(&self, id: &str) -> Result { - self.perform_delete_request(format!("/v1/group/{}", id).as_str()) + tokio_block_on(self.asclient.idm_group_delete(id)) } pub fn idm_group_create(&self, name: &str) -> Result { - let mut new_group = Entry { - attrs: BTreeMap::new(), - }; - new_group - .attrs - .insert("name".to_string(), vec![name.to_string()]); - self.perform_post_request("/v1/group", new_group) - .map(|_: OperationResponse| true) + tokio_block_on(self.asclient.idm_group_create(name)) } // ==== accounts pub fn idm_account_list(&self) -> Result, ClientError> { - self.perform_get_request("/v1/account") + tokio_block_on(self.asclient.idm_account_list()) } pub fn idm_account_create(&self, name: &str, dn: &str) -> Result { - let mut new_acct = Entry { - attrs: BTreeMap::new(), - }; - new_acct - .attrs - .insert("name".to_string(), vec![name.to_string()]); - new_acct - .attrs - .insert("displayname".to_string(), vec![dn.to_string()]); - self.perform_post_request("/v1/account", new_acct) - .map(|_: OperationResponse| true) + tokio_block_on(self.asclient.idm_account_create(name, dn)) + } + + pub fn idm_account_set_password(&self, cleartext: String) -> Result { + tokio_block_on(self.asclient.idm_account_set_password(cleartext)) } pub fn idm_account_set_displayname(&self, id: &str, dn: &str) -> Result { - self.perform_put_request( - format!("/v1/account/{}/_attr/displayname", id).as_str(), - vec![dn.to_string()], - ) + tokio_block_on(self.asclient.idm_account_set_displayname(id, dn)) } pub fn idm_account_delete(&self, id: &str) -> Result { - self.perform_delete_request(format!("/v1/account/{}", id).as_str()) + tokio_block_on(self.asclient.idm_account_delete(id)) } pub fn idm_account_get(&self, id: &str) -> Result, ClientError> { - self.perform_get_request(format!("/v1/account/{}", id).as_str()) + tokio_block_on(self.asclient.idm_account_get(id)) + } + + pub fn idm_account_get_attr( + &self, + id: &str, + attr: &str, + ) -> Result>, ClientError> { + tokio_block_on(self.asclient.idm_account_get_attr(id, attr)) } // different ways to set the primary credential? @@ -923,23 +512,14 @@ impl KanidmClient { id: &str, pw: &str, ) -> Result { - let r = SetCredentialRequest::Password(pw.to_string()); - self.perform_put_request( - format!("/v1/account/{}/_credential/primary", id).as_str(), - r, + tokio_block_on( + self.asclient + .idm_account_primary_credential_set_password(id, pw), ) } - pub fn idm_account_get_attr( - &self, - id: &str, - attr: &str, - ) -> Result>, ClientError> { - self.perform_get_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str()) - } - pub fn idm_account_purge_attr(&self, id: &str, attr: &str) -> Result { - self.perform_delete_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str()) + tokio_block_on(self.asclient.idm_account_purge_attr(id, attr)) } pub fn idm_account_set_attr( @@ -948,8 +528,7 @@ impl KanidmClient { attr: &str, values: &[&str], ) -> Result { - let m: Vec<_> = values.iter().map(|v| (*v).to_string()).collect(); - self.perform_put_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str(), m) + tokio_block_on(self.asclient.idm_account_set_attr(id, attr, values)) } pub fn idm_account_primary_credential_import_password( @@ -957,9 +536,9 @@ impl KanidmClient { id: &str, pw: &str, ) -> Result { - self.perform_put_request( - format!("/v1/account/{}/_attr/password_import", id).as_str(), - vec![pw.to_string()], + tokio_block_on( + self.asclient + .idm_account_primary_credential_import_password(id, pw), ) } @@ -967,16 +546,10 @@ impl KanidmClient { &self, id: &str, ) -> Result { - let r = SetCredentialRequest::GeneratePassword; - let res: Result = self.perform_put_request( - format!("/v1/account/{}/_credential/primary", id).as_str(), - r, - ); - match res { - Ok(SetCredentialResponse::Token(p)) => Ok(p), - Ok(_) => Err(ClientError::EmptyResponse), - Err(e) => Err(e), - } + tokio_block_on( + self.asclient + .idm_account_primary_credential_set_generated(id), + ) } // Reg intent for totp @@ -985,16 +558,10 @@ impl KanidmClient { id: &str, label: &str, ) -> Result<(Uuid, TOTPSecret), ClientError> { - let r = SetCredentialRequest::TOTPGenerate(label.to_string()); - let res: Result = self.perform_put_request( - format!("/v1/account/{}/_credential/primary", id).as_str(), - r, - ); - match res { - Ok(SetCredentialResponse::TOTPCheck(u, s)) => Ok((u, s)), - Ok(_) => Err(ClientError::EmptyResponse), - Err(e) => Err(e), - } + tokio_block_on( + self.asclient + .idm_account_primary_credential_generate_totp(id, label), + ) } // Verify the totp @@ -1004,33 +571,17 @@ impl KanidmClient { otp: u32, session: Uuid, ) -> Result { - let r = SetCredentialRequest::TOTPVerify(session, otp); - let res: Result = self.perform_put_request( - format!("/v1/account/{}/_credential/primary", id).as_str(), - r, - ); - match res { - Ok(SetCredentialResponse::Success) => Ok(true), - Ok(SetCredentialResponse::TOTPCheck(u, s)) => Err(ClientError::TOTPVerifyFailed(u, s)), - Ok(_) => Err(ClientError::EmptyResponse), - Err(e) => Err(e), - } + tokio_block_on( + self.asclient + .idm_account_primary_credential_verify_totp(id, otp, session), + ) } pub fn idm_account_primary_credential_remove_totp( &self, id: &str, ) -> Result { - let r = SetCredentialRequest::TOTPRemove; - let res: Result = self.perform_put_request( - format!("/v1/account/{}/_credential/primary", id).as_str(), - r, - ); - match res { - Ok(SetCredentialResponse::Success) => Ok(true), - Ok(_) => Err(ClientError::EmptyResponse), - Err(e) => Err(e), - } + tokio_block_on(self.asclient.idm_account_primary_credential_remove_totp(id)) } pub fn idm_account_primary_credential_register_webauthn( @@ -1038,16 +589,10 @@ impl KanidmClient { id: &str, label: &str, ) -> Result<(Uuid, CreationChallengeResponse), ClientError> { - let r = SetCredentialRequest::WebauthnBegin(label.to_string()); - let res: Result = self.perform_put_request( - format!("/v1/account/{}/_credential/primary", id).as_str(), - r, - ); - match res { - Ok(SetCredentialResponse::WebauthnCreateChallenge(u, s)) => Ok((u, s)), - Ok(_) => Err(ClientError::EmptyResponse), - Err(e) => Err(e), - } + tokio_block_on( + self.asclient + .idm_account_primary_credential_register_webauthn(id, label), + ) } pub fn idm_account_primary_credential_complete_webuthn_registration( @@ -1056,16 +601,10 @@ impl KanidmClient { rego: RegisterPublicKeyCredential, session: Uuid, ) -> Result<(), ClientError> { - let r = SetCredentialRequest::WebauthnRegister(session, rego); - let res: Result = self.perform_put_request( - format!("/v1/account/{}/_credential/primary", id).as_str(), - r, - ); - match res { - Ok(SetCredentialResponse::Success) => Ok(()), - Ok(_) => Err(ClientError::EmptyResponse), - Err(e) => Err(e), - } + tokio_block_on( + self.asclient + .idm_account_primary_credential_complete_webuthn_registration(id, rego, session), + ) } pub fn idm_account_primary_credential_remove_webauthn( @@ -1073,53 +612,39 @@ impl KanidmClient { id: &str, label: &str, ) -> Result { - let r = SetCredentialRequest::WebauthnRemove(label.to_string()); - let res: Result = self.perform_put_request( - format!("/v1/account/{}/_credential/primary", id).as_str(), - r, - ); - match res { - Ok(SetCredentialResponse::Success) => Ok(true), - Ok(_) => Err(ClientError::EmptyResponse), - Err(e) => Err(e), - } + tokio_block_on( + self.asclient + .idm_account_primary_credential_remove_webauthn(id, label), + ) } pub fn idm_account_get_credential_status( &self, id: &str, ) -> Result { - let res: Result = - self.perform_get_request(format!("/v1/account/{}/_credential/_status", id).as_str()); - res.and_then(|cs| { - if cs.creds.is_empty() { - Err(ClientError::EmptyResponse) - } else { - Ok(cs) - } - }) + tokio_block_on(self.asclient.idm_account_get_credential_status(id)) } pub fn idm_account_radius_credential_get( &self, id: &str, ) -> Result, ClientError> { - self.perform_get_request(format!("/v1/account/{}/_radius", id).as_str()) + tokio_block_on(self.asclient.idm_account_radius_credential_get(id)) } pub fn idm_account_radius_credential_regenerate( &self, id: &str, ) -> Result { - self.perform_post_request(format!("/v1/account/{}/_radius", id).as_str(), ()) + tokio_block_on(self.asclient.idm_account_radius_credential_regenerate(id)) } pub fn idm_account_radius_credential_delete(&self, id: &str) -> Result { - self.perform_delete_request(format!("/v1/account/{}/_radius", id).as_str()) + tokio_block_on(self.asclient.idm_account_radius_credential_delete(id)) } pub fn idm_account_radius_token_get(&self, id: &str) -> Result { - self.perform_get_request(format!("/v1/account/{}/_radius/_token", id).as_str()) + tokio_block_on(self.asclient.idm_account_radius_token_get(id)) } pub fn idm_account_unix_extend( @@ -1128,29 +653,19 @@ impl KanidmClient { gidnumber: Option, shell: Option<&str>, ) -> Result { - let ux = AccountUnixExtend { - shell: shell.map(|s| s.to_string()), - gidnumber, - }; - self.perform_post_request(format!("/v1/account/{}/_unix", id).as_str(), ux) + tokio_block_on(self.asclient.idm_account_unix_extend(id, gidnumber, shell)) } pub fn idm_account_unix_token_get(&self, id: &str) -> Result { - self.perform_get_request(format!("/v1/account/{}/_unix/_token", id).as_str()) + tokio_block_on(self.asclient.idm_account_unix_token_get(id)) } pub fn idm_account_unix_cred_put(&self, id: &str, cred: &str) -> Result { - let req = SingleStringRequest { - value: cred.to_string(), - }; - self.perform_put_request( - format!("/v1/account/{}/_unix/_credential", id).as_str(), - req, - ) + tokio_block_on(self.asclient.idm_account_unix_cred_put(id, cred)) } pub fn idm_account_unix_cred_delete(&self, id: &str) -> Result { - self.perform_delete_request(format!("/v1/account/{}/_unix/_credential", id).as_str()) + tokio_block_on(self.asclient.idm_account_unix_cred_delete(id)) } pub fn idm_account_unix_cred_verify( @@ -1158,14 +673,11 @@ impl KanidmClient { id: &str, cred: &str, ) -> Result, ClientError> { - let req = SingleStringRequest { - value: cred.to_string(), - }; - self.perform_post_request(format!("/v1/account/{}/_unix/_auth", id).as_str(), req) + tokio_block_on(self.asclient.idm_account_unix_cred_verify(id, cred)) } pub fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result, ClientError> { - self.perform_get_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str()) + tokio_block_on(self.asclient.idm_account_get_ssh_pubkeys(id)) } pub fn idm_account_post_ssh_pubkey( @@ -1174,12 +686,11 @@ impl KanidmClient { tag: &str, pubkey: &str, ) -> Result { - let sk = (tag.to_string(), pubkey.to_string()); - self.perform_post_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str(), sk) + tokio_block_on(self.asclient.idm_account_post_ssh_pubkey(id, tag, pubkey)) } pub fn idm_account_person_extend(&self, id: &str) -> Result { - self.perform_post_request(format!("/v1/account/{}/_person/_extend", id).as_str(), ()) + tokio_block_on(self.asclient.idm_account_person_extend(id)) } /* @@ -1193,72 +704,63 @@ impl KanidmClient { id: &str, tag: &str, ) -> Result, ClientError> { - self.perform_get_request(format!("/v1/account/{}/_ssh_pubkeys/{}", id, tag).as_str()) + tokio_block_on(self.asclient.idm_account_get_ssh_pubkey(id, tag)) } pub fn idm_account_delete_ssh_pubkey(&self, id: &str, tag: &str) -> Result { - self.perform_delete_request(format!("/v1/account/{}/_ssh_pubkeys/{}", id, tag).as_str()) + tokio_block_on(self.asclient.idm_account_delete_ssh_pubkey(id, tag)) } // ==== domain_info (aka domain) pub fn idm_domain_list(&self) -> Result, ClientError> { - self.perform_get_request("/v1/domain") + tokio_block_on(self.asclient.idm_domain_list()) } pub fn idm_domain_get(&self, id: &str) -> Result { - self.perform_get_request(format!("/v1/domain/{}", id).as_str()) + tokio_block_on(self.asclient.idm_domain_get(id)) } // pub fn idm_domain_get_attr pub fn idm_domain_get_ssid(&self, id: &str) -> Result { - self.perform_get_request(format!("/v1/domain/{}/_attr/domain_ssid", id).as_str()) - .and_then(|mut r: Vec| - // Get the first result - r.pop() - .ok_or( - ClientError::EmptyResponse - )) + tokio_block_on(self.asclient.idm_domain_get_ssid(id)) } // pub fn idm_domain_put_attr pub fn idm_domain_set_ssid(&self, id: &str, ssid: &str) -> Result { - self.perform_put_request( - format!("/v1/domain/{}/_attr/domain_ssid", id).as_str(), - vec![ssid.to_string()], - ) + tokio_block_on(self.asclient.idm_domain_set_ssid(id, ssid)) } // ==== schema pub fn idm_schema_list(&self) -> Result, ClientError> { - self.perform_get_request("/v1/schema") + tokio_block_on(self.asclient.idm_schema_list()) } pub fn idm_schema_attributetype_list(&self) -> Result, ClientError> { - self.perform_get_request("/v1/schema/attributetype") + tokio_block_on(self.asclient.idm_schema_attributetype_list()) } pub fn idm_schema_attributetype_get(&self, id: &str) -> Result, ClientError> { - self.perform_get_request(format!("/v1/schema/attributetype/{}", id).as_str()) + tokio_block_on(self.asclient.idm_schema_attributetype_get(id)) } pub fn idm_schema_classtype_list(&self) -> Result, ClientError> { - self.perform_get_request("/v1/schema/classtype") + tokio_block_on(self.asclient.idm_schema_classtype_list()) } pub fn idm_schema_classtype_get(&self, id: &str) -> Result, ClientError> { - self.perform_get_request(format!("/v1/schema/classtype/{}", id).as_str()) + tokio_block_on(self.asclient.idm_schema_classtype_get(id)) } // ==== recycle bin pub fn recycle_bin_list(&self) -> Result, ClientError> { - self.perform_get_request("/v1/recycle_bin") + tokio_block_on(self.asclient.recycle_bin_list()) } pub fn recycle_bin_get(&self, id: &str) -> Result, ClientError> { - self.perform_get_request(format!("/v1/recycle_bin/{}", id).as_str()) + tokio_block_on(self.asclient.recycle_bin_get(id)) } pub fn recycle_bin_revive(&self, id: &str) -> Result { - self.perform_post_request(format!("/v1/recycle_bin/{}/_revive", id).as_str(), ()) + tokio_block_on(self.asclient.recycle_bin_revive(id)) } } diff --git a/kanidm_rlm_python/kanidmradius.py b/kanidm_rlm_python/kanidmradius.py index f4439937f..7fdd27640 100644 --- a/kanidm_rlm_python/kanidmradius.py +++ b/kanidm_rlm_python/kanidmradius.py @@ -51,6 +51,9 @@ def _authenticate(s, acct, pw): print(r.json()) raise Exception("AuthInitFailed") + session_id = r.headers["x-kanidm-auth-session-id"] + headers = {"X-KANIDM-AUTH-SESSION-ID": session_id} + # {'sessionid': '00000000-5fe5-46e1-06b6-b830dd035a10', 'state': {'choose': ['password']}} if 'password' not in r.json().get('state', {'choose': None}).get('choose', None): print("invalid auth mech presented %s" % r.json()) @@ -58,13 +61,13 @@ def _authenticate(s, acct, pw): begin_auth = {"step": {"begin": "password"}} - r = s.post(AUTH_URL, json=begin_auth, verify=CA, timeout=TIMEOUT) + r = s.post(AUTH_URL, json=begin_auth, verify=CA, timeout=TIMEOUT, headers=headers) if r.status_code != 200: print(r.json()) raise Exception("AuthBeginFailed") cred_auth = {"step": { "cred": {"password": pw}}} - r = s.post(AUTH_URL, json=cred_auth, verify=CA, timeout=TIMEOUT) + r = s.post(AUTH_URL, json=cred_auth, verify=CA, timeout=TIMEOUT, headers=headers) response = r.json() if r.status_code != 200: print(response) diff --git a/kanidm_unix_int/tests/cache_layer_test.rs b/kanidm_unix_int/tests/cache_layer_test.rs index 20482737c..300804af5 100644 --- a/kanidm_unix_int/tests/cache_layer_test.rs +++ b/kanidm_unix_int/tests/cache_layer_test.rs @@ -526,7 +526,7 @@ fn test_cache_account_pam_allowed() { .await .expect("failed to auth as admin"); adminclient - .idm_group_add_members("allowed_group", vec!["testaccount1"]) + .idm_group_add_members("allowed_group", &["testaccount1"]) .await .unwrap(); diff --git a/kanidmd/server.toml b/kanidmd/server.toml index 18c6a3925..d34ad1507 100644 --- a/kanidmd/server.toml +++ b/kanidmd/server.toml @@ -4,5 +4,5 @@ db_path = "/tmp/kanidm.db" db_fs_type = "zfs" tls_chain = "../insecure/chain.pem" tls_key = "../insecure/key.pem" -log_level = "quiet" +log_level = "verbose" origin = "https://idm.example.com" diff --git a/kanidmd/src/lib/actors/v1_write.rs b/kanidmd/src/lib/actors/v1_write.rs index 14193b018..b522b2a18 100644 --- a/kanidmd/src/lib/actors/v1_write.rs +++ b/kanidmd/src/lib/actors/v1_write.rs @@ -1085,15 +1085,17 @@ impl QueryServerWriteV1 { "class".into(), Value::new_class("posixaccount"), ))) - .chain(iter::once(gidnumber.as_ref().map(|_| { - Modify::Purged("gidnumber".into()) - }))) + .chain(iter::once( + gidnumber + .as_ref() + .map(|_| Modify::Purged("gidnumber".into())), + )) .chain(iter::once(gidnumber.map(|n| { Modify::Present("gidnumber".into(), Value::new_uint32(n)) }))) - .chain(iter::once(shell.as_ref().map(|_| { - Modify::Purged("loginshell".into()) - }))) + .chain(iter::once( + shell.as_ref().map(|_| Modify::Purged("loginshell".into())), + )) .chain(iter::once(shell.map(|s| { Modify::Present("loginshell".into(), Value::new_iutf8(s.as_str())) }))) diff --git a/kanidmd/src/lib/be/idl_sqlite.rs b/kanidmd/src/lib/be/idl_sqlite.rs index 548879a23..0e1ab1321 100644 --- a/kanidmd/src/lib/be/idl_sqlite.rs +++ b/kanidmd/src/lib/be/idl_sqlite.rs @@ -456,7 +456,7 @@ impl IdlSqliteTransaction for IdlSqliteReadTransaction { impl Drop for IdlSqliteReadTransaction { // Abort - so far this has proven reliable to use drop here. - fn drop(self: &mut Self) { + fn drop(&mut self) { if !self.committed { #[allow(clippy::expect_used)] self.conn @@ -496,7 +496,7 @@ impl IdlSqliteTransaction for IdlSqliteWriteTransaction { impl Drop for IdlSqliteWriteTransaction { // Abort - fn drop(self: &mut Self) { + fn drop(&mut self) { if !self.committed { #[allow(clippy::expect_used)] self.conn diff --git a/kanidmd/src/lib/core/https.rs b/kanidmd/src/lib/core/https.rs index 14acd72d7..8658037c1 100644 --- a/kanidmd/src/lib/core/https.rs +++ b/kanidmd/src/lib/core/https.rs @@ -53,6 +53,8 @@ pub struct AppState { pub trait RequestExtensions { fn get_current_uat(&self) -> Option; + fn get_current_auth_session_id(&self) -> Option; + fn get_url_param(&self, param: &str) -> Result; } @@ -81,6 +83,27 @@ impl RequestExtensions for tide::Request { }) } + fn get_current_auth_session_id(&self) -> Option { + // We see if there is a signed header copy first. + let kref = &self.state().fernet_handle; + self.header("X-KANIDM-AUTH-SESSION-ID") + .and_then(|hv| { + // Get the first header value. + hv.get(0) + }) + .and_then(|h| { + // Take the token str and attempt to decrypt + // Attempt to re-inflate a uuid from bytes. + let uat: Option = kref + .decrypt_with_ttl(h.as_str(), 3600) + .ok() + .and_then(|b| serde_json::from_slice(&b).ok()); + uat + }) + // If not there, get from the cookie instead. + .or_else(|| self.session().get::("auth-session-id")) + } + fn get_url_param(&self, param: &str) -> Result { self.param(param) .map(|s| s.to_string()) @@ -1008,26 +1031,24 @@ pub async fn do_nothing(_req: tide::Request) -> tide::Result { Ok(res) } -// We probably need an extract auth or similar to handle the different -// types (cookie, bearer), and to generic this over get/post. - pub async fn auth(mut req: tide::Request) -> tide::Result { - // AuthRequest - // First, deal with some state management. // Do anything here first that's needed like getting the session details // out of the req cookie. - let (eventid, hvalue) = new_eventid!(); - let maybe_sessionid = req.session().get::("auth-session-id"); + + let maybe_sessionid = req.get_current_auth_session_id(); debug!("🍿 {:?}", maybe_sessionid); - let obj: AuthRequest = req.body_json().await - .map_err(|e| {debug!("wat? {:?}", e); e}) - ?; + let obj: AuthRequest = req.body_json().await.map_err(|e| { + debug!("wat? {:?}", e); + e + })?; let auth_msg = AuthMessage::new(obj, maybe_sessionid, eventid); + let mut auth_session_id_tok = None; + // We probably need to know if we allocate the cookie, that this is a // new session, and in that case, anything *except* authrequest init is // invalid. @@ -1054,26 +1075,41 @@ pub async fn auth(mut req: tide::Request) -> tide::Result { AuthState::Choose(allowed) => { debug!("🧩 -> AuthState::Choose"); let msession = req.session_mut(); - // Force a new cookie session. - // msession.regenerate(); + // Ensure the auth-session-id is set msession.remove("auth-session-id"); msession .insert("auth-session-id", sessionid) - .map(|_| ProtoAuthState::Choose(allowed)) .map_err(|_| OperationError::InvalidSessionState) + .and_then(|_| { + let kref = &req.state().fernet_handle; + // Get the header token ready. + serde_json::to_vec(&sessionid) + .map(|data| { + auth_session_id_tok = Some(kref.encrypt(&data)); + }) + .map_err(|_| OperationError::InvalidSessionState) + }) + .map(|_| ProtoAuthState::Choose(allowed)) } AuthState::Continue(allowed) => { debug!("🧩 -> AuthState::Continue"); let msession = req.session_mut(); - // Force a new cookie session. - // msession.regenerate(); // Ensure the auth-session-id is set msession.remove("auth-session-id"); msession .insert("auth-session-id", sessionid) - .map(|_| ProtoAuthState::Continue(allowed)) .map_err(|_| OperationError::InvalidSessionState) + .and_then(|_| { + let kref = &req.state().fernet_handle; + // Get the header token ready. + serde_json::to_vec(&sessionid) + .map(|data| { + auth_session_id_tok = Some(kref.encrypt(&data)); + }) + .map_err(|_| OperationError::InvalidSessionState) + }) + .map(|_| ProtoAuthState::Continue(allowed)) } AuthState::Success(uat) => { debug!("🧩 -> AuthState::Success"); @@ -1102,7 +1138,14 @@ pub async fn auth(mut req: tide::Request) -> tide::Result { Err(e) => Err(e), }; - to_tide_response(res, hvalue) + to_tide_response(res, hvalue).map(|mut res| { + // if the sessionid was injected into our cookie, set it in the + // header too. + if let Some(tok) = auth_session_id_tok { + res.insert_header("X-KANIDM-AUTH-SESSION-ID", tok); + } + res + }) } pub async fn idm_account_set_password(mut req: tide::Request) -> tide::Result { diff --git a/kanidmd/src/lib/server.rs b/kanidmd/src/lib/server.rs index 401c1c61c..fe46b3698 100644 --- a/kanidmd/src/lib/server.rs +++ b/kanidmd/src/lib/server.rs @@ -189,14 +189,10 @@ pub trait QueryServerTransaction { // the QS wr/ro to the plugin trait. However, there shouldn't be a need for search // plugis, because all data transforms should be in the write path. - let res = self - .get_be_txn() - .search(au, lims, &vfr) - .map(|r| r) - .map_err(|e| { - ladmin_error!(au, "backend failure -> {:?}", e); - OperationError::Backend - })?; + let res = self.get_be_txn().search(au, lims, &vfr).map_err(|e| { + ladmin_error!(au, "backend failure -> {:?}", e); + OperationError::Backend + })?; // Apply ACP before we let the plugins "have at it". // WARNING; for external searches this is NOT the only diff --git a/kanidmd_web_ui/src/lib.rs b/kanidmd_web_ui/src/lib.rs index 3656229c5..606528c94 100644 --- a/kanidmd_web_ui/src/lib.rs +++ b/kanidmd_web_ui/src/lib.rs @@ -1,4 +1,4 @@ -#![recursion_limit="256"] +#![recursion_limit = "256"] use wasm_bindgen::prelude::*; use yew::prelude::*; diff --git a/kanidmd_web_ui/src/login.rs b/kanidmd_web_ui/src/login.rs index 18d596e04..9fb714747 100644 --- a/kanidmd_web_ui/src/login.rs +++ b/kanidmd_web_ui/src/login.rs @@ -1,9 +1,9 @@ -use wasm_bindgen::prelude::*; use anyhow::Error; +use wasm_bindgen::prelude::*; use yew::format::{Json, Nothing}; use yew::prelude::*; -use yew::services::{ConsoleService, StorageService}; use yew::services::fetch::{FetchService, FetchTask, Request, Response}; +use yew::services::{ConsoleService, StorageService}; use kanidm_proto::v1::{AuthRequest, AuthState, AuthStep}; @@ -24,32 +24,24 @@ pub enum LoginAppMsg { impl LoginApp { fn auth_begin(&mut self) { let username_copy = self.username.clone(); - let callback = self.link.callback( - move |response: Response>>| { - let (parts, body) = response.into_parts(); - match body { - Json(Ok(state)) => { - LoginAppMsg::Next(state) + let callback = + self.link + .callback(move |response: Response>>| { + let (parts, body) = response.into_parts(); + match body { + Json(Ok(state)) => LoginAppMsg::Next(state), + Json(Err(_)) => LoginAppMsg::DoNothing, } - Json(Err(_)) => { - LoginAppMsg::DoNothing - } - } - } - ); + }); let authreq = AuthRequest { - step: AuthStep::Init(self.username.clone()) + step: AuthStep::Init(self.username.clone()), }; // Setup the auth step::init(username); self.ft = Request::post("/v1/auth") .header("Content-Type", "application/json") .body(Json(&authreq)) .map_err(|_| ()) - .and_then(|request| { - FetchService::fetch_binary(request, callback) - .map_err(|_| ()) - - }) + .and_then(|request| FetchService::fetch_binary(request, callback).map_err(|_| ())) .map(|ft| Some(ft)) .unwrap_or_else(|_e| None); } @@ -63,9 +55,7 @@ impl Component for LoginApp { ConsoleService::log(format!("create").as_str()); // First we need to work out what state we are in. - let lstorage = StorageService::new( - yew::services::storage::Area::Local - ).unwrap(); + let lstorage = StorageService::new(yew::services::storage::Area::Local).unwrap(); // Get any previous sessions? // Are they still valid? @@ -74,7 +64,7 @@ impl Component for LoginApp { link, username: "".to_string(), lstorage, - ft: None + ft: None, } } @@ -98,9 +88,7 @@ impl Component for LoginApp { ConsoleService::log(format!("next -> {:?}", state).as_str()); true } - LoginAppMsg::DoNothing => { - false - } + LoginAppMsg::DoNothing => false, } }