kanidm_client/
lib.rs

1#![deny(warnings)]
2#![warn(unused_extern_crates)]
3#![deny(clippy::todo)]
4#![deny(clippy::unimplemented)]
5#![deny(clippy::unwrap_used)]
6#![deny(clippy::expect_used)]
7#![deny(clippy::panic)]
8#![deny(clippy::unreachable)]
9#![deny(clippy::await_holding_lock)]
10#![deny(clippy::needless_pass_by_value)]
11#![deny(clippy::trivially_copy_pass_by_ref)]
12
13#[macro_use]
14extern crate tracing;
15
16use std::collections::{BTreeMap, BTreeSet as Set};
17use std::fmt::{Debug, Display, Formatter};
18use std::fs::File;
19#[cfg(target_family = "unix")] // not needed for windows builds
20use std::fs::{metadata, Metadata};
21use std::io::{ErrorKind, Read};
22#[cfg(target_family = "unix")] // not needed for windows builds
23use std::os::unix::fs::MetadataExt;
24use std::path::Path;
25use std::sync::Arc;
26use std::time::Duration;
27
28use compact_jwt::Jwk;
29
30pub use http;
31use kanidm_proto::constants::uri::V1_AUTH_VALID;
32use kanidm_proto::constants::{
33    ATTR_DOMAIN_DISPLAY_NAME, ATTR_DOMAIN_LDAP_BASEDN, ATTR_DOMAIN_SSID, ATTR_ENTRY_MANAGED_BY,
34    ATTR_KEY_ACTION_REVOKE, ATTR_LDAP_ALLOW_UNIX_PW_BIND, ATTR_LDAP_MAX_QUERYABLE_ATTRS, ATTR_NAME,
35    CLIENT_TOKEN_CACHE, KOPID, KSESSIONID, KVERSION,
36};
37use kanidm_proto::internal::*;
38use kanidm_proto::v1::*;
39use reqwest::cookie::{CookieStore, Jar};
40use reqwest::Response;
41pub use reqwest::StatusCode;
42use serde::de::DeserializeOwned;
43use serde::{Deserialize, Serialize};
44use serde_json::error::Error as SerdeJsonError;
45use serde_urlencoded::ser::Error as UrlEncodeError;
46use tokio::sync::{Mutex, RwLock};
47use url::Url;
48use uuid::Uuid;
49use webauthn_rs_proto::{
50    PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
51};
52
53mod application;
54mod domain;
55mod group;
56mod message;
57mod oauth;
58mod person;
59mod schema;
60mod scim;
61mod service_account;
62mod sync_account;
63mod system;
64
65const EXPECT_VERSION: &str = env!("CARGO_PKG_VERSION");
66
67#[derive(Debug)]
68pub enum ClientError {
69    Unauthorized,
70    SessionExpired,
71    Http(reqwest::StatusCode, Option<OperationError>, String),
72    Transport(reqwest::Error),
73    AuthenticationFailed,
74    EmptyResponse,
75    TotpVerifyFailed(Uuid, TotpSecret),
76    TotpInvalidSha1(Uuid),
77    JsonDecode(reqwest::Error, String),
78    InvalidResponseFormat(String),
79    JsonEncode(SerdeJsonError),
80    UrlEncode(UrlEncodeError),
81    SystemError,
82    ConfigParseIssue(String),
83    CertParseIssue(String),
84    UntrustedCertificate(String),
85    InvalidRequest(String),
86}
87
88/// Settings describing a single instance.
89#[derive(Debug, Deserialize, Serialize)]
90pub struct KanidmClientConfigInstance {
91    /// The URL of the server, ie `https://example.com`.
92    ///
93    /// Environment variable is `KANIDM_URL`. Yeah, we know.
94    pub uri: Option<String>,
95    /// Whether to verify the TLS certificate of the server matches the hostname you connect to, defaults to `true`.
96    ///
97    /// Environment variable is slightly inverted - `KANIDM_SKIP_HOSTNAME_VERIFICATION`.
98    pub verify_hostnames: Option<bool>,
99    /// Whether to verify the Certificate Authority details of the server's TLS certificate, defaults to `true`.
100    ///
101    /// Environment variable is slightly inverted - `KANIDM_ACCEPT_INVALID_CERTS`.
102    pub verify_ca: Option<bool>,
103    /// Optionally you can specify the path of a CA certificate to use for verifying the server, if you're not using one trusted by your system certificate store.
104    ///
105    /// Environment variable is `KANIDM_CA_PATH`.
106    pub ca_path: Option<String>,
107
108    /// Connection Timeout for the client, in seconds.
109    pub connect_timeout: Option<u64>,
110}
111
112#[derive(Debug, Deserialize, Serialize)]
113/// This struct is what Kanidm uses for parsing the client configuration at runtime.
114///
115/// # Configuration file inheritance
116///
117/// The configuration files are loaded in order, with the last one loaded overriding the previous one.
118///
119/// 1. The "system" config is loaded from in [kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH].
120/// 2. Then a per-user configuration, from [kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH_HOME] is loaded.
121/// 3. All of these may be overridden by setting environment variables.
122///
123pub struct KanidmClientConfig {
124    // future editors, please leave this public so others can parse the config!
125    #[serde(flatten)]
126    pub default: KanidmClientConfigInstance,
127
128    #[serde(flatten)]
129    // future editors, please leave this public so others can parse the config!
130    pub instances: BTreeMap<String, KanidmClientConfigInstance>,
131}
132
133#[derive(Debug, Clone, Default)]
134pub struct KanidmClientBuilder {
135    address: Option<String>,
136    verify_ca: bool,
137    verify_hostnames: bool,
138    ca: Option<reqwest::Certificate>,
139    connect_timeout: Option<u64>,
140    request_timeout: Option<u64>,
141    use_system_proxies: bool,
142    /// Where to store auth tokens, only use in testing!
143    token_cache_path: Option<String>,
144    disable_system_ca_store: bool,
145}
146
147impl Display for KanidmClientBuilder {
148    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
149        match &self.address {
150            Some(value) => writeln!(f, "address: {value}")?,
151            None => writeln!(f, "address: unset")?,
152        }
153        writeln!(f, "verify_ca: {}", self.verify_ca)?;
154        writeln!(f, "verify_hostnames: {}", self.verify_hostnames)?;
155        match &self.ca {
156            Some(value) => writeln!(f, "ca: {value:#?}")?,
157            None => writeln!(f, "ca: unset")?,
158        }
159        match self.connect_timeout {
160            Some(value) => writeln!(f, "connect_timeout: {value}")?,
161            None => writeln!(f, "connect_timeout: unset")?,
162        }
163        match self.request_timeout {
164            Some(value) => writeln!(f, "request_timeout: {value}")?,
165            None => writeln!(f, "request_timeout: unset")?,
166        }
167        writeln!(f, "use_system_proxies: {}", self.use_system_proxies)?;
168        writeln!(
169            f,
170            "token_cache_path: {}",
171            self.token_cache_path
172                .clone()
173                .unwrap_or(CLIENT_TOKEN_CACHE.to_string())
174        )
175    }
176}
177
178#[derive(Debug)]
179pub struct KanidmClient {
180    pub(crate) client: reqwest::Client,
181    client_cookies: Arc<Jar>,
182    pub(crate) addr: String,
183    pub(crate) origin: Url,
184    pub(crate) builder: KanidmClientBuilder,
185    pub(crate) bearer_token: RwLock<Option<String>>,
186    pub(crate) auth_session_id: RwLock<Option<String>>,
187    pub(crate) check_version: Mutex<bool>,
188    /// Where to store the tokens when you auth, only modify in testing.
189    token_cache_path: String,
190}
191
192#[cfg(target_family = "unix")]
193fn read_file_metadata<P: AsRef<Path>>(path: &P) -> Result<Metadata, ()> {
194    metadata(path).map_err(|e| {
195        error!(
196            "Unable to read metadata for {} - {:?}",
197            path.as_ref().to_str().unwrap_or("Alert: invalid path"),
198            e
199        );
200    })
201}
202
203impl KanidmClientBuilder {
204    pub fn new() -> Self {
205        KanidmClientBuilder {
206            address: None,
207            verify_ca: true,
208            verify_hostnames: true,
209            ca: None,
210            connect_timeout: None,
211            request_timeout: None,
212            use_system_proxies: true,
213            token_cache_path: None,
214            disable_system_ca_store: false,
215        }
216    }
217
218    fn parse_certificate(ca_path: &str) -> Result<reqwest::Certificate, ClientError> {
219        let mut buf = Vec::new();
220        // Is the CA secure?
221        #[cfg(target_family = "windows")]
222        warn!("File metadata checks on Windows aren't supported right now, this could be a security risk.");
223
224        #[cfg(target_family = "unix")]
225        {
226            let path = Path::new(ca_path);
227            let ca_meta = read_file_metadata(&path).map_err(|e| {
228                error!("{:?}", e);
229                ClientError::ConfigParseIssue(format!("{e:?}"))
230            })?;
231
232            trace!("uid:gid {}:{}", ca_meta.uid(), ca_meta.gid());
233
234            #[cfg(not(debug_assertions))]
235            if ca_meta.uid() != 0 || ca_meta.gid() != 0 {
236                warn!(
237                    "{} should be owned be root:root to prevent tampering",
238                    ca_path
239                );
240            }
241
242            trace!("mode={:o}", ca_meta.mode());
243            if (ca_meta.mode() & 0o7133) != 0 {
244                warn!("permissions on {} are NOT secure. 0644 is a secure default. Should not be setuid, executable or allow group/other writes.", ca_path);
245            }
246        }
247
248        let mut f = File::open(ca_path).map_err(|e| {
249            error!("{:?}", e);
250            ClientError::ConfigParseIssue(format!("{e:?}"))
251        })?;
252        f.read_to_end(&mut buf).map_err(|e| {
253            error!("{:?}", e);
254            ClientError::ConfigParseIssue(format!("{e:?}"))
255        })?;
256        reqwest::Certificate::from_pem(&buf).map_err(|e| {
257            error!("{:?}", e);
258            ClientError::CertParseIssue(format!("{e:?}"))
259        })
260    }
261
262    fn apply_config_options(self, kcc: KanidmClientConfigInstance) -> Result<Self, ClientError> {
263        let KanidmClientBuilder {
264            address,
265            verify_ca,
266            verify_hostnames,
267            ca,
268            connect_timeout,
269            request_timeout,
270            use_system_proxies,
271            token_cache_path,
272            disable_system_ca_store,
273        } = self;
274        // Process and apply all our options if they exist.
275        let address = match kcc.uri {
276            Some(uri) => Some(uri),
277            None => {
278                debug!("No URI in config supplied to apply_config_options");
279                address
280            }
281        };
282        let verify_ca = kcc.verify_ca.unwrap_or(verify_ca);
283        let verify_hostnames = kcc.verify_hostnames.unwrap_or(verify_hostnames);
284        let ca = match kcc.ca_path {
285            Some(ca_path) => Some(Self::parse_certificate(&ca_path)?),
286            None => ca,
287        };
288        let connect_timeout = kcc.connect_timeout.or(connect_timeout);
289
290        Ok(KanidmClientBuilder {
291            address,
292            verify_ca,
293            verify_hostnames,
294            ca,
295            connect_timeout,
296            request_timeout,
297            use_system_proxies,
298            token_cache_path,
299            disable_system_ca_store,
300        })
301    }
302
303    pub fn read_options_from_optional_config<P: AsRef<Path> + std::fmt::Debug>(
304        self,
305        config_path: P,
306    ) -> Result<Self, ClientError> {
307        self.read_options_from_optional_instance_config(config_path, None)
308    }
309
310    pub fn read_options_from_optional_instance_config<P: AsRef<Path> + std::fmt::Debug>(
311        self,
312        config_path: P,
313        instance: Option<&str>,
314    ) -> Result<Self, ClientError> {
315        debug!(
316            "Attempting to load {} instance configuration from {:#?}",
317            instance.unwrap_or("default"),
318            &config_path
319        );
320
321        // We have to check the .exists case manually, because there are some weird overlayfs
322        // issues in docker where when the file does NOT exist, but we "open it" we get an
323        // error describing that the file is actually a directory rather than a not exists
324        // error. This check enforces that we get the CORRECT error message instead.
325        if !config_path.as_ref().exists() {
326            debug!("{:?} does not exist", config_path);
327            let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
328            debug!(%diag);
329            return Ok(self);
330        };
331
332        // If the file does not exist, we skip this function.
333        let mut f = match File::open(&config_path) {
334            Ok(f) => {
335                debug!("Successfully opened configuration file {:#?}", &config_path);
336                f
337            }
338            Err(e) => {
339                match e.kind() {
340                    ErrorKind::NotFound => {
341                        debug!(
342                            "Configuration file {:#?} not found, skipping.",
343                            &config_path
344                        );
345                    }
346                    ErrorKind::PermissionDenied => {
347                        warn!(
348                            "Permission denied loading configuration file {:#?}, skipping.",
349                            &config_path
350                        );
351                    }
352                    _ => {
353                        debug!(
354                            "Unable to open config file {:#?} [{:?}], skipping ...",
355                            &config_path, e
356                        );
357                    }
358                };
359                let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
360                info!(%diag);
361
362                return Ok(self);
363            }
364        };
365
366        let mut contents = String::new();
367        f.read_to_string(&mut contents).map_err(|e| {
368            error!("{:?}", e);
369            ClientError::ConfigParseIssue(format!("{e:?}"))
370        })?;
371
372        let mut config: KanidmClientConfig = toml::from_str(&contents).map_err(|e| {
373            error!("{:?}", e);
374            ClientError::ConfigParseIssue(format!("{e:?}"))
375        })?;
376
377        if let Some(instance_name) = instance {
378            if let Some(instance_config) = config.instances.remove(instance_name) {
379                self.apply_config_options(instance_config)
380            } else {
381                info!(
382                    "instance {} does not exist in config file {}",
383                    instance_name,
384                    config_path.as_ref().display()
385                );
386
387                // It's not an error if the instance isn't present, the build step
388                // will fail if there is insufficient information to proceed.
389                Ok(self)
390            }
391        } else {
392            self.apply_config_options(config.default)
393        }
394    }
395
396    pub fn address(self, address: String) -> Self {
397        KanidmClientBuilder {
398            address: Some(address),
399            ..self
400        }
401    }
402
403    /// Enable or disable the native ca roots. By default these roots are enabled.
404    pub fn enable_native_ca_roots(self, enable: bool) -> Self {
405        KanidmClientBuilder {
406            // We have to flip the bool state here due to Default on bool being false
407            // and we want our options to be positive to a native speaker.
408            disable_system_ca_store: !enable,
409            ..self
410        }
411    }
412
413    pub fn danger_accept_invalid_hostnames(self, accept_invalid_hostnames: bool) -> Self {
414        KanidmClientBuilder {
415            // We have to flip the bool state here due to english language.
416            verify_hostnames: !accept_invalid_hostnames,
417            ..self
418        }
419    }
420
421    pub fn danger_accept_invalid_certs(self, accept_invalid_certs: bool) -> Self {
422        KanidmClientBuilder {
423            // We have to flip the bool state here due to english language.
424            verify_ca: !accept_invalid_certs,
425            ..self
426        }
427    }
428
429    pub fn connect_timeout(self, secs: u64) -> Self {
430        KanidmClientBuilder {
431            connect_timeout: Some(secs),
432            ..self
433        }
434    }
435
436    pub fn request_timeout(self, secs: u64) -> Self {
437        KanidmClientBuilder {
438            request_timeout: Some(secs),
439            ..self
440        }
441    }
442
443    pub fn no_proxy(self) -> Self {
444        KanidmClientBuilder {
445            use_system_proxies: false,
446            ..self
447        }
448    }
449
450    pub fn set_token_cache_path(self, token_cache_path: Option<String>) -> Self {
451        KanidmClientBuilder {
452            token_cache_path,
453            ..self
454        }
455    }
456
457    #[allow(clippy::result_unit_err)]
458    pub fn add_root_certificate_filepath(self, ca_path: &str) -> Result<Self, ClientError> {
459        //Okay we have a ca to add. Let's read it in and setup.
460        let ca = Self::parse_certificate(ca_path).map_err(|e| {
461            error!("{:?}", e);
462            ClientError::CertParseIssue(format!("{e:?}"))
463        })?;
464
465        Ok(KanidmClientBuilder {
466            ca: Some(ca),
467            ..self
468        })
469    }
470
471    fn display_warnings(&self, address: &str) {
472        // Check for problems now
473        if !self.verify_ca {
474            warn!("verify_ca set to false in client configuration - this may allow network interception of passwords!");
475        }
476
477        if !self.verify_hostnames {
478            warn!(
479                "verify_hostnames set to false in client configuration - this may allow network interception of passwords!"
480            );
481        }
482        if !address.starts_with("https://") {
483            warn!("Address does not start with 'https://' - this may allow network interception of passwords!");
484        }
485    }
486
487    /// Generates a useragent header based on the package name and version
488    pub fn user_agent() -> &'static str {
489        static APP_USER_AGENT: &str =
490            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
491        APP_USER_AGENT
492    }
493
494    /*
495    /// Consume self and return an async client.
496    pub fn build(self) -> Result<KanidmClient, reqwest::Error> {
497        self.build_async().map(|asclient| KanidmClient { asclient })
498    }
499    */
500
501    /// Build the client ready for usage.
502    pub fn build(self) -> Result<KanidmClient, ClientError> {
503        // Errghh, how to handle this cleaner.
504        let address = match &self.address {
505            Some(a) => a.clone(),
506            None => {
507                error!("Configuration option 'uri' missing from client configuration, cannot continue client startup without specifying a server to connect to. 🤔");
508                return Err(ClientError::ConfigParseIssue(
509                    "Configuration option 'uri' missing from client configuration, cannot continue client startup without specifying a server to connect to. 🤔".to_string(),
510                ));
511            }
512        };
513
514        self.display_warnings(&address);
515
516        let client_cookies = Arc::new(Jar::default());
517
518        let mut client_builder = reqwest::Client::builder()
519            .user_agent(KanidmClientBuilder::user_agent())
520            // We don't directly use cookies, but it may be required for load balancers that
521            // implement sticky sessions with cookies.
522            .cookie_store(true)
523            .cookie_provider(client_cookies.clone());
524
525        if let Some(cacert) = self.ca.as_ref() {
526            if self.disable_system_ca_store {
527                client_builder = client_builder.tls_certs_only(vec![cacert.clone()])
528            }
529        };
530
531        client_builder = client_builder
532            // .tls_built_in_native_certs(!self.disable_system_ca_store)
533            .danger_accept_invalid_hostnames(!self.verify_hostnames)
534            .danger_accept_invalid_certs(!self.verify_ca);
535
536        let client_builder = match self.use_system_proxies {
537            true => client_builder,
538            false => client_builder.no_proxy(),
539        };
540
541        let client_builder = match &self.ca {
542            Some(cert) => client_builder.add_root_certificate(cert.clone()),
543            None => client_builder,
544        };
545
546        let client_builder = match &self.connect_timeout {
547            Some(secs) => client_builder.connect_timeout(Duration::from_secs(*secs)),
548            None => client_builder,
549        };
550
551        let client_builder = match &self.request_timeout {
552            Some(secs) => client_builder.timeout(Duration::from_secs(*secs)),
553            None => client_builder,
554        };
555
556        let client = client_builder.build().map_err(ClientError::Transport)?;
557
558        // Now get the origin.
559        #[allow(clippy::expect_used)]
560        let uri = Url::parse(&address).expect("failed to parse address");
561
562        #[allow(clippy::expect_used)]
563        let origin =
564            Url::parse(&uri.origin().ascii_serialization()).expect("failed to parse origin");
565
566        let token_cache_path = match self.token_cache_path.clone() {
567            Some(val) => val.to_string(),
568            None => CLIENT_TOKEN_CACHE.to_string(),
569        };
570
571        Ok(KanidmClient {
572            client,
573            client_cookies,
574            addr: address,
575            builder: self,
576            bearer_token: RwLock::new(None),
577            auth_session_id: RwLock::new(None),
578            origin,
579            check_version: Mutex::new(true),
580            token_cache_path,
581        })
582    }
583}
584
585/// This is probably pretty jank but it works and was pulled from here:
586/// <https://github.com/seanmonstar/reqwest/issues/1602#issuecomment-1220996681>
587fn find_reqwest_error_source<E: std::error::Error + 'static>(
588    orig: &dyn std::error::Error,
589) -> Option<&E> {
590    let mut cause = orig.source();
591    while let Some(err) = cause {
592        if let Some(typed) = err.downcast_ref::<E>() {
593            return Some(typed);
594        }
595        cause = err.source();
596    }
597
598    // else
599    None
600}
601
602impl KanidmClient {
603    /// Access the underlying reqwest client that has been configured for this Kanidm server
604    pub fn client(&self) -> &reqwest::Client {
605        &self.client
606    }
607
608    pub fn get_origin(&self) -> &Url {
609        &self.origin
610    }
611
612    /// Returns the base URL of the server
613    pub fn get_url(&self) -> Url {
614        #[allow(clippy::panic)]
615        match self.addr.parse::<Url>() {
616            Ok(val) => val,
617            Err(err) => panic!("Failed to parse {} into URL: {:?}", self.addr, err),
618        }
619    }
620
621    /// Get a URL based on adding an endpoint to the base URL of the server
622    pub fn make_url(&self, endpoint: &str) -> Url {
623        #[allow(clippy::expect_used)]
624        self.get_url().join(endpoint).expect("Failed to join URL")
625    }
626
627    pub async fn set_token(&self, new_token: String) {
628        let mut tguard = self.bearer_token.write().await;
629        *tguard = Some(new_token);
630    }
631
632    pub async fn get_token(&self) -> Option<String> {
633        let tguard = self.bearer_token.read().await;
634        (*tguard).as_ref().cloned()
635    }
636
637    pub fn new_session(&self) -> Result<Self, ClientError> {
638        // Copy our builder, and then just process it.
639        let builder = self.builder.clone();
640        builder.build()
641    }
642
643    pub async fn logout(&self) -> Result<(), ClientError> {
644        match self.perform_get_request("/v1/logout").await {
645            Err(ClientError::Unauthorized)
646            | Err(ClientError::Http(reqwest::StatusCode::UNAUTHORIZED, _, _))
647            | Ok(()) => {
648                let mut tguard = self.bearer_token.write().await;
649                *tguard = None;
650                Ok(())
651            }
652            e => e,
653        }
654    }
655
656    pub fn get_token_cache_path(&self) -> String {
657        self.token_cache_path.clone()
658    }
659
660    /// Check that we're getting the right version back from the server.
661    async fn expect_version(&self, response: &reqwest::Response) {
662        let mut guard = self.check_version.lock().await;
663
664        if !*guard {
665            return;
666        }
667
668        if response.status() == StatusCode::BAD_GATEWAY
669            || response.status() == StatusCode::GATEWAY_TIMEOUT
670        {
671            // don't need to check versions when there's an intermediary reporting connectivity
672            debug!("Gateway error in response - we're going through a proxy so the version check is skipped.");
673            *guard = false;
674            return;
675        }
676
677        let ver: &str = response
678            .headers()
679            .get(KVERSION)
680            .and_then(|hv| hv.to_str().ok())
681            .unwrap_or("");
682
683        let matching = ver == EXPECT_VERSION;
684
685        if !matching {
686            warn!(server_version = ?ver, client_version = ?EXPECT_VERSION, "Mismatched client and server version - features may not work, or other unforeseen errors may occur.")
687        }
688
689        #[cfg(any(test, debug_assertions))]
690        if !matching && std::env::var("KANIDM_DEV_YOLO").is_err() {
691            eprintln!("⚠️  You're in debug/dev mode, so we're going to quit here.");
692            eprintln!("If you really must do this, set KANIDM_DEV_YOLO=1");
693            std::process::exit(1);
694        }
695
696        // Check is done once, mark as no longer needing to occur
697        *guard = false;
698    }
699
700    /// You've got the response from a reqwest and you want to turn it into a `ClientError`
701    pub fn handle_response_error(&self, error: reqwest::Error) -> ClientError {
702        if error.is_connect() {
703            if find_reqwest_error_source::<std::io::Error>(&error).is_some() {
704                // TODO: one day handle IO errors better
705                trace!("Got an IO error! {:?}", &error);
706                return ClientError::Transport(error);
707            }
708            if let Some(hyper_error) = find_reqwest_error_source::<hyper::Error>(&error) {
709                // hyper errors can be *anything* depending on the underlying client libraries
710                // ref: https://github.com/hyperium/hyper/blob/9feb70e9249d9fb99634ec96f83566e6bb3b3128/src/error.rs#L26C2-L26C2
711                if format!("{hyper_error:?}")
712                    .to_lowercase()
713                    .contains("certificate")
714                {
715                    return ClientError::UntrustedCertificate(format!("{hyper_error}"));
716                }
717            }
718        }
719        ClientError::Transport(error)
720    }
721
722    fn get_kopid_from_response(&self, response: &Response) -> String {
723        let opid = response
724            .headers()
725            .get(KOPID)
726            .and_then(|hv| hv.to_str().ok())
727            .unwrap_or("missing_kopid")
728            .to_string();
729
730        debug!("opid -> {:?}", opid);
731        opid
732    }
733
734    async fn perform_simple_post_request<R: Serialize, T: DeserializeOwned>(
735        &self,
736        dest: &str,
737        request: &R,
738    ) -> Result<T, ClientError> {
739        let response = self.client.post(self.make_url(dest)).json(request);
740
741        let response = response
742            .send()
743            .await
744            .map_err(|err| self.handle_response_error(err))?;
745
746        self.expect_version(&response).await;
747
748        let opid = self.get_kopid_from_response(&response);
749
750        self.ok_or_clienterror(&opid, response)
751            .await?
752            .json()
753            .await
754            .map_err(|e| ClientError::JsonDecode(e, opid))
755    }
756
757    async fn perform_auth_post_request<R: Serialize, T: DeserializeOwned>(
758        &self,
759        dest: &str,
760        request: R,
761    ) -> Result<T, ClientError> {
762        trace!("perform_auth_post_request connecting to {}", dest);
763
764        let auth_url = self.make_url(dest);
765
766        let response = self.client.post(auth_url.clone()).json(&request);
767
768        // If we have a bearer token, set it now.
769        let response = {
770            let tguard = self.bearer_token.read().await;
771            if let Some(token) = &(*tguard) {
772                response.bearer_auth(token)
773            } else {
774                response
775            }
776        };
777
778        // If we have a session header, set it now. This is only used when connecting
779        // to an older server.
780        let response = {
781            let sguard = self.auth_session_id.read().await;
782            if let Some(sessionid) = &(*sguard) {
783                response.header(KSESSIONID, sessionid)
784            } else {
785                response
786            }
787        };
788
789        let response = response
790            .send()
791            .await
792            .map_err(|err| self.handle_response_error(err))?;
793
794        self.expect_version(&response).await;
795
796        // If we have a sessionid header in the response, get it now.
797        let opid = self.get_kopid_from_response(&response);
798
799        let response = self.ok_or_clienterror(&opid, response).await?;
800
801        // Do we have a cookie? Our job here isn't to parse and validate the cookies, but just to
802        // know if the session id was set *in* our cookie store at all.
803        let cookie_present = self
804            .client_cookies
805            .cookies(&auth_url)
806            .map(|cookie_header| {
807                cookie_header
808                    .to_str()
809                    .ok()
810                    .map(|cookie_str| {
811                        cookie_str
812                            .split(';')
813                            .filter_map(|c| c.split_once('='))
814                            .any(|(name, _)| name == COOKIE_AUTH_SESSION_ID)
815                    })
816                    .unwrap_or_default()
817            })
818            .unwrap_or_default();
819
820        {
821            let headers = response.headers();
822
823            let mut sguard = self.auth_session_id.write().await;
824            trace!(?cookie_present);
825            if cookie_present {
826                // Clear and auth session id if present, we have the cookie instead.
827                *sguard = None;
828            } else {
829                // This situation occurs when a newer client connects to an older server
830                debug!("Auth SessionID cookie not present, falling back to header.");
831                *sguard = headers
832                    .get(KSESSIONID)
833                    .and_then(|hv| hv.to_str().ok().map(str::to_string));
834            }
835        }
836
837        response
838            .json()
839            .await
840            .map_err(|e| ClientError::JsonDecode(e, opid))
841    }
842
843    pub async fn perform_post_request<R: Serialize, T: DeserializeOwned>(
844        &self,
845        dest: &str,
846        request: R,
847    ) -> Result<T, ClientError> {
848        let response = self.client.post(self.make_url(dest)).json(&request);
849
850        let response = {
851            let tguard = self.bearer_token.read().await;
852            if let Some(token) = &(*tguard) {
853                response.bearer_auth(token)
854            } else {
855                response
856            }
857        };
858
859        let response = response
860            .send()
861            .await
862            .map_err(|err| self.handle_response_error(err))?;
863
864        self.expect_version(&response).await;
865
866        let opid = self.get_kopid_from_response(&response);
867
868        self.ok_or_clienterror(&opid, response)
869            .await?
870            .json()
871            .await
872            .map_err(|e| ClientError::JsonDecode(e, opid))
873    }
874
875    async fn perform_put_request<R: Serialize, T: DeserializeOwned>(
876        &self,
877        dest: &str,
878        request: R,
879    ) -> Result<T, ClientError> {
880        let response = self.client.put(self.make_url(dest)).json(&request);
881
882        let response = {
883            let tguard = self.bearer_token.read().await;
884            if let Some(token) = &(*tguard) {
885                response.bearer_auth(token)
886            } else {
887                response
888            }
889        };
890
891        let response = response
892            .send()
893            .await
894            .map_err(|err| self.handle_response_error(err))?;
895
896        self.expect_version(&response).await;
897
898        let opid = self.get_kopid_from_response(&response);
899
900        match response.status() {
901            reqwest::StatusCode::OK => {}
902            reqwest::StatusCode::UNPROCESSABLE_ENTITY => {
903                return Err(ClientError::InvalidRequest(format!("Something about the request content was invalid, check the server logs for further information. Operation ID: {} Error: {:?}",opid, response.text().await.ok() )))
904            }
905
906            unexpected => {
907                return Err(ClientError::Http(
908                    unexpected,
909                    response.json().await.ok(),
910                    opid,
911                ))
912            }
913        }
914
915        response
916            .json()
917            .await
918            .map_err(|e| ClientError::JsonDecode(e, opid))
919    }
920
921    /// Handles not-OK responses and turns them into ClientErrors
922    async fn ok_or_clienterror(
923        &self,
924        opid: &str,
925        response: reqwest::Response,
926    ) -> Result<reqwest::Response, ClientError> {
927        match response.status() {
928            reqwest::StatusCode::OK => Ok(response),
929            unexpected => Err(ClientError::Http(
930                unexpected,
931                response.json().await.ok(),
932                opid.to_string(),
933            )),
934        }
935    }
936
937    pub async fn perform_patch_request<R: Serialize, T: DeserializeOwned>(
938        &self,
939        dest: &str,
940        request: R,
941    ) -> Result<T, ClientError> {
942        let response = self.client.patch(self.make_url(dest)).json(&request);
943
944        let response = {
945            let tguard = self.bearer_token.read().await;
946            if let Some(token) = &(*tguard) {
947                response.bearer_auth(token)
948            } else {
949                response
950            }
951        };
952
953        let response = response
954            .send()
955            .await
956            .map_err(|err| self.handle_response_error(err))?;
957
958        self.expect_version(&response).await;
959
960        let opid = self.get_kopid_from_response(&response);
961
962        self.ok_or_clienterror(&opid, response)
963            .await?
964            .json()
965            .await
966            .map_err(|e| ClientError::JsonDecode(e, opid))
967    }
968
969    #[instrument(level = "debug", skip(self))]
970    pub async fn perform_get_request<T: DeserializeOwned>(
971        &self,
972        dest: &str,
973    ) -> Result<T, ClientError> {
974        let query: Option<()> = None;
975        self.perform_get_request_query(dest, query).await
976    }
977
978    #[instrument(level = "debug", skip(self))]
979    pub async fn perform_get_request_query<T: DeserializeOwned, Q: Serialize + Debug>(
980        &self,
981        dest: &str,
982        query: Option<Q>,
983    ) -> Result<T, ClientError> {
984        let mut dest_url = self.make_url(dest);
985
986        if let Some(query) = query {
987            let txt = serde_urlencoded::to_string(&query).map_err(ClientError::UrlEncode)?;
988
989            if !txt.is_empty() {
990                dest_url.set_query(Some(txt.as_str()));
991            }
992        }
993
994        let response = self.client.get(dest_url);
995        let response = {
996            let tguard = self.bearer_token.read().await;
997            if let Some(token) = &(*tguard) {
998                response.bearer_auth(token)
999            } else {
1000                response
1001            }
1002        };
1003
1004        let response = response
1005            .send()
1006            .await
1007            .map_err(|err| self.handle_response_error(err))?;
1008
1009        self.expect_version(&response).await;
1010
1011        let opid = self.get_kopid_from_response(&response);
1012
1013        self.ok_or_clienterror(&opid, response)
1014            .await?
1015            .json()
1016            .await
1017            .map_err(|e| ClientError::JsonDecode(e, opid))
1018    }
1019
1020    async fn perform_delete_request(&self, dest: &str) -> Result<(), ClientError> {
1021        let response = self
1022            .client
1023            .delete(self.make_url(dest))
1024            // empty-ish body that makes the parser happy
1025            .json(&serde_json::json!([]));
1026
1027        let response = {
1028            let tguard = self.bearer_token.read().await;
1029            if let Some(token) = &(*tguard) {
1030                response.bearer_auth(token)
1031            } else {
1032                response
1033            }
1034        };
1035
1036        let response = response
1037            .send()
1038            .await
1039            .map_err(|err| self.handle_response_error(err))?;
1040
1041        self.expect_version(&response).await;
1042
1043        let opid = self.get_kopid_from_response(&response);
1044
1045        self.ok_or_clienterror(&opid, response)
1046            .await?
1047            .json()
1048            .await
1049            .map_err(|e| ClientError::JsonDecode(e, opid))
1050    }
1051
1052    async fn perform_delete_request_with_body<R: Serialize>(
1053        &self,
1054        dest: &str,
1055        request: R,
1056    ) -> Result<(), ClientError> {
1057        let response = self.client.delete(self.make_url(dest)).json(&request);
1058
1059        let response = {
1060            let tguard = self.bearer_token.read().await;
1061            if let Some(token) = &(*tguard) {
1062                response.bearer_auth(token)
1063            } else {
1064                response
1065            }
1066        };
1067
1068        let response = response
1069            .send()
1070            .await
1071            .map_err(|err| self.handle_response_error(err))?;
1072
1073        self.expect_version(&response).await;
1074
1075        let opid = self.get_kopid_from_response(&response);
1076
1077        self.ok_or_clienterror(&opid, response)
1078            .await?
1079            .json()
1080            .await
1081            .map_err(|e| ClientError::JsonDecode(e, opid))
1082    }
1083
1084    #[instrument(level = "debug", skip(self))]
1085    pub async fn auth_step_init(&self, ident: &str) -> Result<Set<AuthMech>, ClientError> {
1086        let auth_init = AuthRequest {
1087            step: AuthStep::Init2 {
1088                username: ident.to_string(),
1089                issue: AuthIssueSession::Token,
1090                privileged: false,
1091            },
1092        };
1093
1094        let r: Result<AuthResponse, _> =
1095            self.perform_auth_post_request("/v1/auth", auth_init).await;
1096        r.map(|v| {
1097            debug!("Authentication Session ID -> {:?}", v.sessionid);
1098            // Stash the session ID header.
1099            v.state
1100        })
1101        .and_then(|state| match state {
1102            AuthState::Choose(mechs) => Ok(mechs),
1103            _ => Err(ClientError::AuthenticationFailed),
1104        })
1105        .map(|mechs| mechs.into_iter().collect())
1106    }
1107
1108    #[instrument(level = "debug", skip(self))]
1109    pub async fn auth_step_begin(&self, mech: AuthMech) -> Result<Vec<AuthAllowed>, ClientError> {
1110        let auth_begin = AuthRequest {
1111            step: AuthStep::Begin(mech),
1112        };
1113
1114        let r: Result<AuthResponse, _> =
1115            self.perform_auth_post_request("/v1/auth", auth_begin).await;
1116        r.map(|v| {
1117            debug!("Authentication Session ID -> {:?}", v.sessionid);
1118            v.state
1119        })
1120        .and_then(|state| match state {
1121            AuthState::Continue(allowed) => Ok(allowed),
1122            _ => Err(ClientError::AuthenticationFailed),
1123        })
1124        // For converting to a Set
1125        // .map(|allowed| allowed.into_iter().collect())
1126    }
1127
1128    #[instrument(level = "debug", skip_all)]
1129    pub async fn auth_step_anonymous(&self) -> Result<AuthResponse, ClientError> {
1130        let auth_anon = AuthRequest {
1131            step: AuthStep::Cred(AuthCredential::Anonymous),
1132        };
1133        let r: Result<AuthResponse, _> =
1134            self.perform_auth_post_request("/v1/auth", auth_anon).await;
1135
1136        if let Ok(ar) = &r {
1137            if let AuthState::Success(token) = &ar.state {
1138                self.set_token(token.clone()).await;
1139            };
1140        };
1141        r
1142    }
1143
1144    #[instrument(level = "debug", skip_all)]
1145    pub async fn auth_step_password(&self, password: &str) -> Result<AuthResponse, ClientError> {
1146        let auth_req = AuthRequest {
1147            step: AuthStep::Cred(AuthCredential::Password(password.to_string())),
1148        };
1149        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1150
1151        if let Ok(ar) = &r {
1152            if let AuthState::Success(token) = &ar.state {
1153                self.set_token(token.clone()).await;
1154            };
1155        };
1156        r
1157    }
1158
1159    #[instrument(level = "debug", skip_all)]
1160    pub async fn auth_step_backup_code(
1161        &self,
1162        backup_code: &str,
1163    ) -> Result<AuthResponse, ClientError> {
1164        let auth_req = AuthRequest {
1165            step: AuthStep::Cred(AuthCredential::BackupCode(backup_code.to_string())),
1166        };
1167        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1168
1169        if let Ok(ar) = &r {
1170            if let AuthState::Success(token) = &ar.state {
1171                self.set_token(token.clone()).await;
1172            };
1173        };
1174        r
1175    }
1176
1177    #[instrument(level = "debug", skip_all)]
1178    pub async fn auth_step_totp(&self, totp: u32) -> Result<AuthResponse, ClientError> {
1179        let auth_req = AuthRequest {
1180            step: AuthStep::Cred(AuthCredential::Totp(totp)),
1181        };
1182        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1183
1184        if let Ok(ar) = &r {
1185            if let AuthState::Success(token) = &ar.state {
1186                self.set_token(token.clone()).await;
1187            };
1188        };
1189        r
1190    }
1191
1192    #[instrument(level = "debug", skip_all)]
1193    pub async fn auth_step_securitykey_complete(
1194        &self,
1195        pkc: Box<PublicKeyCredential>,
1196    ) -> Result<AuthResponse, ClientError> {
1197        let auth_req = AuthRequest {
1198            step: AuthStep::Cred(AuthCredential::SecurityKey(pkc)),
1199        };
1200        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1201
1202        if let Ok(ar) = &r {
1203            if let AuthState::Success(token) = &ar.state {
1204                self.set_token(token.clone()).await;
1205            };
1206        };
1207        r
1208    }
1209
1210    #[instrument(level = "debug", skip_all)]
1211    pub async fn auth_step_passkey_complete(
1212        &self,
1213        pkc: Box<PublicKeyCredential>,
1214    ) -> Result<AuthResponse, ClientError> {
1215        let auth_req = AuthRequest {
1216            step: AuthStep::Cred(AuthCredential::Passkey(pkc)),
1217        };
1218        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1219
1220        if let Ok(ar) = &r {
1221            if let AuthState::Success(token) = &ar.state {
1222                self.set_token(token.clone()).await;
1223            };
1224        };
1225        r
1226    }
1227
1228    #[instrument(level = "debug", skip(self))]
1229    pub async fn auth_anonymous(&self) -> Result<(), ClientError> {
1230        let mechs = match self.auth_step_init("anonymous").await {
1231            Ok(s) => s,
1232            Err(e) => return Err(e),
1233        };
1234
1235        if !mechs.contains(&AuthMech::Anonymous) {
1236            debug!("Anonymous mech not presented");
1237            return Err(ClientError::AuthenticationFailed);
1238        }
1239
1240        let _state = match self.auth_step_begin(AuthMech::Anonymous).await {
1241            Ok(s) => s,
1242            Err(e) => return Err(e),
1243        };
1244
1245        let r = self.auth_step_anonymous().await?;
1246
1247        match r.state {
1248            AuthState::Success(token) => {
1249                self.set_token(token.clone()).await;
1250                Ok(())
1251            }
1252            _ => Err(ClientError::AuthenticationFailed),
1253        }
1254    }
1255
1256    #[instrument(level = "debug", skip(self, password))]
1257    pub async fn auth_simple_password(
1258        &self,
1259        ident: &str,
1260        password: &str,
1261    ) -> Result<(), ClientError> {
1262        trace!("Init auth step");
1263        let mechs = match self.auth_step_init(ident).await {
1264            Ok(s) => s,
1265            Err(e) => return Err(e),
1266        };
1267
1268        if !mechs.contains(&AuthMech::Password) {
1269            debug!("Password mech not presented");
1270            return Err(ClientError::AuthenticationFailed);
1271        }
1272
1273        let _state = match self.auth_step_begin(AuthMech::Password).await {
1274            Ok(s) => s,
1275            Err(e) => return Err(e),
1276        };
1277
1278        let r = self.auth_step_password(password).await?;
1279
1280        match r.state {
1281            AuthState::Success(_) => Ok(()),
1282            _ => Err(ClientError::AuthenticationFailed),
1283        }
1284    }
1285
1286    #[instrument(level = "debug", skip(self, password, totp))]
1287    pub async fn auth_password_totp(
1288        &self,
1289        ident: &str,
1290        password: &str,
1291        totp: u32,
1292    ) -> Result<(), ClientError> {
1293        let mechs = match self.auth_step_init(ident).await {
1294            Ok(s) => s,
1295            Err(e) => return Err(e),
1296        };
1297
1298        if !mechs.contains(&AuthMech::PasswordTotp) {
1299            debug!("PasswordTotp mech not presented");
1300            return Err(ClientError::AuthenticationFailed);
1301        }
1302
1303        let state = match self.auth_step_begin(AuthMech::PasswordTotp).await {
1304            Ok(s) => s,
1305            Err(e) => return Err(e),
1306        };
1307
1308        if !state.contains(&AuthAllowed::Totp) {
1309            debug!("TOTP step not offered.");
1310            return Err(ClientError::AuthenticationFailed);
1311        }
1312
1313        let r = self.auth_step_totp(totp).await?;
1314
1315        // Should need to continue.
1316        match r.state {
1317            AuthState::Continue(allowed) => {
1318                if !allowed.contains(&AuthAllowed::Password) {
1319                    debug!("Password step not offered.");
1320                    return Err(ClientError::AuthenticationFailed);
1321                }
1322            }
1323            _ => {
1324                debug!("Invalid AuthState presented.");
1325                return Err(ClientError::AuthenticationFailed);
1326            }
1327        };
1328
1329        let r = self.auth_step_password(password).await?;
1330
1331        match r.state {
1332            AuthState::Success(_token) => Ok(()),
1333            _ => Err(ClientError::AuthenticationFailed),
1334        }
1335    }
1336
1337    #[instrument(level = "debug", skip(self, password, backup_code))]
1338    pub async fn auth_password_backup_code(
1339        &self,
1340        ident: &str,
1341        password: &str,
1342        backup_code: &str,
1343    ) -> Result<(), ClientError> {
1344        let mechs = match self.auth_step_init(ident).await {
1345            Ok(s) => s,
1346            Err(e) => return Err(e),
1347        };
1348
1349        if !mechs.contains(&AuthMech::PasswordBackupCode) {
1350            debug!("PasswordBackupCode mech not presented");
1351            return Err(ClientError::AuthenticationFailed);
1352        }
1353
1354        let state = match self.auth_step_begin(AuthMech::PasswordBackupCode).await {
1355            Ok(s) => s,
1356            Err(e) => return Err(e),
1357        };
1358
1359        if !state.contains(&AuthAllowed::BackupCode) {
1360            debug!("Backup Code step not offered.");
1361            return Err(ClientError::AuthenticationFailed);
1362        }
1363
1364        let r = self.auth_step_backup_code(backup_code).await?;
1365
1366        // Should need to continue.
1367        match r.state {
1368            AuthState::Continue(allowed) => {
1369                if !allowed.contains(&AuthAllowed::Password) {
1370                    debug!("Password step not offered.");
1371                    return Err(ClientError::AuthenticationFailed);
1372                }
1373            }
1374            _ => {
1375                debug!("Invalid AuthState presented.");
1376                return Err(ClientError::AuthenticationFailed);
1377            }
1378        };
1379
1380        let r = self.auth_step_password(password).await?;
1381
1382        match r.state {
1383            AuthState::Success(_token) => Ok(()),
1384            _ => Err(ClientError::AuthenticationFailed),
1385        }
1386    }
1387
1388    #[instrument(level = "debug", skip(self))]
1389    pub async fn auth_passkey_begin(
1390        &self,
1391        ident: &str,
1392    ) -> Result<RequestChallengeResponse, ClientError> {
1393        let mechs = match self.auth_step_init(ident).await {
1394            Ok(s) => s,
1395            Err(e) => return Err(e),
1396        };
1397
1398        if !mechs.contains(&AuthMech::Passkey) {
1399            debug!("Webauthn mech not presented");
1400            return Err(ClientError::AuthenticationFailed);
1401        }
1402
1403        let state = match self.auth_step_begin(AuthMech::Passkey).await {
1404            Ok(mut s) => s.pop(),
1405            Err(e) => return Err(e),
1406        };
1407
1408        // State is now a set of auth continues.
1409        match state {
1410            Some(AuthAllowed::Passkey(r)) => Ok(r),
1411            _ => Err(ClientError::AuthenticationFailed),
1412        }
1413    }
1414
1415    #[instrument(level = "debug", skip_all)]
1416    pub async fn auth_passkey_complete(
1417        &self,
1418        pkc: Box<PublicKeyCredential>,
1419    ) -> Result<(), ClientError> {
1420        let r = self.auth_step_passkey_complete(pkc).await?;
1421        match r.state {
1422            AuthState::Success(_token) => Ok(()),
1423            _ => Err(ClientError::AuthenticationFailed),
1424        }
1425    }
1426
1427    pub async fn reauth_begin(&self) -> Result<Vec<AuthAllowed>, ClientError> {
1428        let issue = AuthIssueSession::Token;
1429        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/reauth", issue).await;
1430
1431        r.map(|v| {
1432            debug!("Authentication Session ID -> {:?}", v.sessionid);
1433            v.state
1434        })
1435        .and_then(|state| match state {
1436            AuthState::Continue(allowed) => Ok(allowed),
1437            _ => Err(ClientError::AuthenticationFailed),
1438        })
1439    }
1440
1441    #[instrument(level = "debug", skip_all)]
1442    pub async fn reauth_simple_password(&self, password: &str) -> Result<(), ClientError> {
1443        let state = match self.reauth_begin().await {
1444            Ok(mut s) => s.pop(),
1445            Err(e) => return Err(e),
1446        };
1447
1448        match state {
1449            Some(AuthAllowed::Password) => {}
1450            _ => {
1451                return Err(ClientError::AuthenticationFailed);
1452            }
1453        };
1454
1455        let r = self.auth_step_password(password).await?;
1456
1457        match r.state {
1458            AuthState::Success(_) => Ok(()),
1459            _ => Err(ClientError::AuthenticationFailed),
1460        }
1461    }
1462
1463    #[instrument(level = "debug", skip_all)]
1464    pub async fn reauth_password_totp(&self, password: &str, totp: u32) -> Result<(), ClientError> {
1465        let state = match self.reauth_begin().await {
1466            Ok(s) => s,
1467            Err(e) => return Err(e),
1468        };
1469
1470        if !state.contains(&AuthAllowed::Totp) {
1471            debug!("TOTP step not offered.");
1472            return Err(ClientError::AuthenticationFailed);
1473        }
1474
1475        let r = self.auth_step_totp(totp).await?;
1476
1477        // Should need to continue.
1478        match r.state {
1479            AuthState::Continue(allowed) => {
1480                if !allowed.contains(&AuthAllowed::Password) {
1481                    debug!("Password step not offered.");
1482                    return Err(ClientError::AuthenticationFailed);
1483                }
1484            }
1485            _ => {
1486                debug!("Invalid AuthState presented.");
1487                return Err(ClientError::AuthenticationFailed);
1488            }
1489        };
1490
1491        let r = self.auth_step_password(password).await?;
1492
1493        match r.state {
1494            AuthState::Success(_token) => Ok(()),
1495            _ => Err(ClientError::AuthenticationFailed),
1496        }
1497    }
1498
1499    #[instrument(level = "debug", skip_all)]
1500    pub async fn reauth_passkey_begin(&self) -> Result<RequestChallengeResponse, ClientError> {
1501        let state = match self.reauth_begin().await {
1502            Ok(mut s) => s.pop(),
1503            Err(e) => return Err(e),
1504        };
1505
1506        // State is now a set of auth continues.
1507        match state {
1508            Some(AuthAllowed::Passkey(r)) => Ok(r),
1509            _ => Err(ClientError::AuthenticationFailed),
1510        }
1511    }
1512
1513    #[instrument(level = "debug", skip_all)]
1514    pub async fn reauth_passkey_complete(
1515        &self,
1516        pkc: Box<PublicKeyCredential>,
1517    ) -> Result<(), ClientError> {
1518        let r = self.auth_step_passkey_complete(pkc).await?;
1519        match r.state {
1520            AuthState::Success(_token) => Ok(()),
1521            _ => Err(ClientError::AuthenticationFailed),
1522        }
1523    }
1524
1525    pub async fn auth_valid(&self) -> Result<(), ClientError> {
1526        self.perform_get_request(V1_AUTH_VALID).await
1527    }
1528
1529    pub async fn get_public_jwk(&self, key_id: &str) -> Result<Jwk, ClientError> {
1530        self.perform_get_request(&format!("/v1/jwk/{key_id}")).await
1531    }
1532
1533    pub async fn whoami(&self) -> Result<Option<Entry>, ClientError> {
1534        let response = self.client.get(self.make_url("/v1/self"));
1535
1536        let response = {
1537            let tguard = self.bearer_token.read().await;
1538            if let Some(token) = &(*tguard) {
1539                response.bearer_auth(token)
1540            } else {
1541                response
1542            }
1543        };
1544
1545        let response = response
1546            .send()
1547            .await
1548            .map_err(|err| self.handle_response_error(err))?;
1549
1550        self.expect_version(&response).await;
1551
1552        let opid = self.get_kopid_from_response(&response);
1553        match response.status() {
1554            // Continue to process.
1555            reqwest::StatusCode::OK => {}
1556            reqwest::StatusCode::UNAUTHORIZED => return Ok(None),
1557            unexpected => {
1558                return Err(ClientError::Http(
1559                    unexpected,
1560                    response.json().await.ok(),
1561                    opid,
1562                ))
1563            }
1564        }
1565
1566        let r: WhoamiResponse = response
1567            .json()
1568            .await
1569            .map_err(|e| ClientError::JsonDecode(e, opid))?;
1570
1571        Ok(Some(r.youare))
1572    }
1573
1574    // Raw DB actions
1575    pub async fn search(&self, filter: Filter) -> Result<Vec<Entry>, ClientError> {
1576        let sr = SearchRequest { filter };
1577        let r: Result<SearchResponse, _> = self.perform_post_request("/v1/raw/search", sr).await;
1578        r.map(|v| v.entries)
1579    }
1580
1581    pub async fn create(&self, entries: Vec<Entry>) -> Result<(), ClientError> {
1582        let c = CreateRequest { entries };
1583        self.perform_post_request("/v1/raw/create", c).await
1584    }
1585
1586    pub async fn modify(&self, filter: Filter, modlist: ModifyList) -> Result<(), ClientError> {
1587        let mr = ModifyRequest { filter, modlist };
1588        self.perform_post_request("/v1/raw/modify", mr).await
1589    }
1590
1591    pub async fn delete(&self, filter: Filter) -> Result<(), ClientError> {
1592        let dr = DeleteRequest { filter };
1593        self.perform_post_request("/v1/raw/delete", dr).await
1594    }
1595
1596    // === idm actions here ==
1597
1598    // ===== GROUPS
1599    pub async fn idm_group_list(&self) -> Result<Vec<Entry>, ClientError> {
1600        self.perform_get_request("/v1/group").await
1601    }
1602
1603    pub async fn idm_group_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
1604        self.perform_get_request(&format!("/v1/group/{id}")).await
1605    }
1606
1607    pub async fn idm_group_get_members(
1608        &self,
1609        id: &str,
1610    ) -> Result<Option<Vec<String>>, ClientError> {
1611        self.perform_get_request(&format!("/v1/group/{id}/_attr/member"))
1612            .await
1613    }
1614
1615    pub async fn idm_group_create(
1616        &self,
1617        name: &str,
1618        entry_managed_by: Option<&str>,
1619    ) -> Result<(), ClientError> {
1620        let mut new_group = Entry {
1621            attrs: BTreeMap::new(),
1622        };
1623        new_group
1624            .attrs
1625            .insert(ATTR_NAME.to_string(), vec![name.to_string()]);
1626
1627        if let Some(entry_manager) = entry_managed_by {
1628            new_group.attrs.insert(
1629                ATTR_ENTRY_MANAGED_BY.to_string(),
1630                vec![entry_manager.to_string()],
1631            );
1632        }
1633
1634        self.perform_post_request("/v1/group", new_group).await
1635    }
1636
1637    pub async fn idm_group_set_entry_managed_by(
1638        &self,
1639        id: &str,
1640        entry_manager: &str,
1641    ) -> Result<(), ClientError> {
1642        let data = vec![entry_manager];
1643        self.perform_put_request(&format!("/v1/group/{id}/_attr/entry_managed_by"), data)
1644            .await
1645    }
1646
1647    pub async fn idm_group_set_members(
1648        &self,
1649        id: &str,
1650        members: &[&str],
1651    ) -> Result<(), ClientError> {
1652        let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect();
1653        self.perform_put_request(&format!("/v1/group/{id}/_attr/member"), m)
1654            .await
1655    }
1656
1657    pub async fn idm_group_add_members(
1658        &self,
1659        id: &str,
1660        members: &[&str],
1661    ) -> Result<(), ClientError> {
1662        let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect();
1663        self.perform_post_request(&format!("/v1/group/{id}/_attr/member"), m)
1664            .await
1665    }
1666
1667    pub async fn idm_group_remove_members(
1668        &self,
1669        group: &str,
1670        members: &[&str],
1671    ) -> Result<(), ClientError> {
1672        debug!(
1673            "Asked to remove members {} from {}",
1674            &members.join(","),
1675            group
1676        );
1677        self.perform_delete_request_with_body(&format!("/v1/group/{group}/_attr/member"), &members)
1678            .await
1679    }
1680
1681    pub async fn idm_group_purge_members(&self, id: &str) -> Result<(), ClientError> {
1682        self.perform_delete_request(&format!("/v1/group/{id}/_attr/member"))
1683            .await
1684    }
1685
1686    pub async fn idm_group_unix_extend(
1687        &self,
1688        id: &str,
1689        gidnumber: Option<u32>,
1690    ) -> Result<(), ClientError> {
1691        let gx = GroupUnixExtend { gidnumber };
1692        self.perform_post_request(&format!("/v1/group/{id}/_unix"), gx)
1693            .await
1694    }
1695
1696    pub async fn idm_group_unix_token_get(&self, id: &str) -> Result<UnixGroupToken, ClientError> {
1697        self.perform_get_request(&format!("/v1/group/{id}/_unix/_token"))
1698            .await
1699    }
1700
1701    pub async fn idm_group_delete(&self, id: &str) -> Result<(), ClientError> {
1702        self.perform_delete_request(&format!("/v1/group/{id}"))
1703            .await
1704    }
1705
1706    // ==== ACCOUNTS
1707
1708    pub async fn idm_account_unix_token_get(&self, id: &str) -> Result<UnixUserToken, ClientError> {
1709        self.perform_get_request(&format!("/v1/account/{id}/_unix/_token"))
1710            .await
1711    }
1712
1713    #[instrument(level = "debug", skip(self))]
1714    pub async fn idm_person_account_credential_update_send_intent(
1715        &self,
1716        id: &str,
1717        ttl: Option<u64>,
1718        email: Option<String>,
1719    ) -> Result<(), ClientError> {
1720        let req = CUIntentSend { ttl, email };
1721        self.perform_post_request(
1722            &format!("/v1/person/{id}/_credential/_update_intent_send"),
1723            req,
1724        )
1725        .await
1726    }
1727
1728    // == new credential update session code.
1729    #[instrument(level = "debug", skip(self))]
1730    pub async fn idm_person_account_credential_update_intent(
1731        &self,
1732        id: &str,
1733        ttl: Option<u32>,
1734    ) -> Result<CUIntentToken, ClientError> {
1735        if let Some(ttl) = ttl {
1736            self.perform_get_request(&format!("/v1/person/{id}/_credential/_update_intent/{ttl}"))
1737                .await
1738        } else {
1739            self.perform_get_request(&format!("/v1/person/{id}/_credential/_update_intent"))
1740                .await
1741        }
1742    }
1743
1744    pub async fn idm_account_credential_update_begin(
1745        &self,
1746        id: &str,
1747    ) -> Result<(CUSessionToken, CUStatus), ClientError> {
1748        self.perform_get_request(&format!("/v1/person/{id}/_credential/_update"))
1749            .await
1750    }
1751
1752    pub async fn idm_account_credential_update_exchange(
1753        &self,
1754        intent_token: String,
1755    ) -> Result<(CUSessionToken, CUStatus), ClientError> {
1756        // We don't need to send the UAT with these, which is why we use the different path.
1757        self.perform_simple_post_request("/v1/credential/_exchange_intent", &intent_token)
1758            .await
1759    }
1760
1761    pub async fn idm_account_credential_update_status(
1762        &self,
1763        session_token: &CUSessionToken,
1764    ) -> Result<CUStatus, ClientError> {
1765        self.perform_simple_post_request("/v1/credential/_status", &session_token)
1766            .await
1767    }
1768
1769    pub async fn idm_account_credential_update_set_password(
1770        &self,
1771        session_token: &CUSessionToken,
1772        pw: &str,
1773    ) -> Result<CUStatus, ClientError> {
1774        let scr = CURequest::Password(pw.to_string());
1775        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1776            .await
1777    }
1778
1779    pub async fn idm_account_credential_update_cancel_mfareg(
1780        &self,
1781        session_token: &CUSessionToken,
1782    ) -> Result<CUStatus, ClientError> {
1783        let scr = CURequest::CancelMFAReg;
1784        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1785            .await
1786    }
1787
1788    pub async fn idm_account_credential_update_init_totp(
1789        &self,
1790        session_token: &CUSessionToken,
1791    ) -> Result<CUStatus, ClientError> {
1792        let scr = CURequest::TotpGenerate;
1793        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1794            .await
1795    }
1796
1797    pub async fn idm_account_credential_update_check_totp(
1798        &self,
1799        session_token: &CUSessionToken,
1800        totp_chal: u32,
1801        label: &str,
1802    ) -> Result<CUStatus, ClientError> {
1803        let scr = CURequest::TotpVerify(totp_chal, label.to_string());
1804        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1805            .await
1806    }
1807
1808    // TODO: add test coverage
1809    pub async fn idm_account_credential_update_accept_sha1_totp(
1810        &self,
1811        session_token: &CUSessionToken,
1812    ) -> Result<CUStatus, ClientError> {
1813        let scr = CURequest::TotpAcceptSha1;
1814        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1815            .await
1816    }
1817
1818    pub async fn idm_account_credential_update_remove_totp(
1819        &self,
1820        session_token: &CUSessionToken,
1821        label: &str,
1822    ) -> Result<CUStatus, ClientError> {
1823        let scr = CURequest::TotpRemove(label.to_string());
1824        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1825            .await
1826    }
1827
1828    // TODO: add test coverage
1829    pub async fn idm_account_credential_update_backup_codes_generate(
1830        &self,
1831        session_token: &CUSessionToken,
1832    ) -> Result<CUStatus, ClientError> {
1833        let scr = CURequest::BackupCodeGenerate;
1834        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1835            .await
1836    }
1837
1838    // TODO: add test coverage
1839    pub async fn idm_account_credential_update_primary_remove(
1840        &self,
1841        session_token: &CUSessionToken,
1842    ) -> Result<CUStatus, ClientError> {
1843        let scr = CURequest::PrimaryRemove;
1844        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1845            .await
1846    }
1847
1848    pub async fn idm_account_credential_update_set_unix_password(
1849        &self,
1850        session_token: &CUSessionToken,
1851        pw: &str,
1852    ) -> Result<CUStatus, ClientError> {
1853        let scr = CURequest::UnixPassword(pw.to_string());
1854        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1855            .await
1856    }
1857
1858    pub async fn idm_account_credential_update_unix_remove(
1859        &self,
1860        session_token: &CUSessionToken,
1861    ) -> Result<CUStatus, ClientError> {
1862        let scr = CURequest::UnixPasswordRemove;
1863        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1864            .await
1865    }
1866
1867    pub async fn idm_account_credential_update_sshkey_add(
1868        &self,
1869        session_token: &CUSessionToken,
1870        label: String,
1871        key: SshPublicKey,
1872    ) -> Result<CUStatus, ClientError> {
1873        let scr = CURequest::SshPublicKey(label, key);
1874        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1875            .await
1876    }
1877
1878    pub async fn idm_account_credential_update_sshkey_remove(
1879        &self,
1880        session_token: &CUSessionToken,
1881        label: String,
1882    ) -> Result<CUStatus, ClientError> {
1883        let scr = CURequest::SshPublicKeyRemove(label);
1884        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1885            .await
1886    }
1887
1888    pub async fn idm_account_credential_update_passkey_init(
1889        &self,
1890        session_token: &CUSessionToken,
1891    ) -> Result<CUStatus, ClientError> {
1892        let scr = CURequest::PasskeyInit;
1893        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1894            .await
1895    }
1896
1897    pub async fn idm_account_credential_update_passkey_finish(
1898        &self,
1899        session_token: &CUSessionToken,
1900        label: String,
1901        registration: RegisterPublicKeyCredential,
1902    ) -> Result<CUStatus, ClientError> {
1903        let scr = CURequest::PasskeyFinish(label, registration);
1904        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1905            .await
1906    }
1907
1908    // TODO: add test coverage
1909    pub async fn idm_account_credential_update_passkey_remove(
1910        &self,
1911        session_token: &CUSessionToken,
1912        uuid: Uuid,
1913    ) -> Result<CUStatus, ClientError> {
1914        let scr = CURequest::PasskeyRemove(uuid);
1915        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1916            .await
1917    }
1918
1919    pub async fn idm_account_credential_update_attested_passkey_init(
1920        &self,
1921        session_token: &CUSessionToken,
1922    ) -> Result<CUStatus, ClientError> {
1923        let scr = CURequest::AttestedPasskeyInit;
1924        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1925            .await
1926    }
1927
1928    pub async fn idm_account_credential_update_attested_passkey_finish(
1929        &self,
1930        session_token: &CUSessionToken,
1931        label: String,
1932        registration: RegisterPublicKeyCredential,
1933    ) -> Result<CUStatus, ClientError> {
1934        let scr = CURequest::AttestedPasskeyFinish(label, registration);
1935        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1936            .await
1937    }
1938
1939    pub async fn idm_account_credential_update_attested_passkey_remove(
1940        &self,
1941        session_token: &CUSessionToken,
1942        uuid: Uuid,
1943    ) -> Result<CUStatus, ClientError> {
1944        let scr = CURequest::AttestedPasskeyRemove(uuid);
1945        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1946            .await
1947    }
1948
1949    pub async fn idm_account_credential_update_commit(
1950        &self,
1951        session_token: &CUSessionToken,
1952    ) -> Result<(), ClientError> {
1953        self.perform_simple_post_request("/v1/credential/_commit", &session_token)
1954            .await
1955    }
1956
1957    // == radius
1958
1959    pub async fn idm_account_radius_token_get(
1960        &self,
1961        id: &str,
1962    ) -> Result<RadiusAuthToken, ClientError> {
1963        self.perform_get_request(&format!("/v1/account/{id}/_radius/_token"))
1964            .await
1965    }
1966
1967    pub async fn idm_account_unix_cred_verify(
1968        &self,
1969        id: &str,
1970        cred: &str,
1971    ) -> Result<Option<UnixUserToken>, ClientError> {
1972        let req = SingleStringRequest {
1973            value: cred.to_string(),
1974        };
1975        self.perform_post_request(&format!("/v1/account/{id}/_unix/_auth"), req)
1976            .await
1977    }
1978
1979    // == generic ssh key handlers
1980    // These return the ssh keys in their "authorized keys" form rather than a format that
1981    // shows labels and can be easily updated.
1982    pub async fn idm_account_get_ssh_pubkey(
1983        &self,
1984        id: &str,
1985        tag: &str,
1986    ) -> Result<Option<String>, ClientError> {
1987        self.perform_get_request(&format!("/v1/account/{id}/_ssh_pubkeys/{tag}"))
1988            .await
1989    }
1990
1991    pub async fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result<Vec<String>, ClientError> {
1992        self.perform_get_request(&format!("/v1/account/{id}/_ssh_pubkeys"))
1993            .await
1994    }
1995
1996    // ==== domain_info (aka domain)
1997    pub async fn idm_domain_get(&self) -> Result<Entry, ClientError> {
1998        let r: Result<Vec<Entry>, ClientError> = self.perform_get_request("/v1/domain").await;
1999        r.and_then(|mut v| v.pop().ok_or(ClientError::EmptyResponse))
2000    }
2001
2002    /// Sets the domain display name using a PUT request
2003    pub async fn idm_domain_set_display_name(
2004        &self,
2005        new_display_name: &str,
2006    ) -> Result<(), ClientError> {
2007        self.perform_put_request(
2008            &format!("/v1/domain/_attr/{ATTR_DOMAIN_DISPLAY_NAME}"),
2009            vec![new_display_name],
2010        )
2011        .await
2012    }
2013
2014    pub async fn idm_domain_set_ldap_basedn(&self, new_basedn: &str) -> Result<(), ClientError> {
2015        self.perform_put_request(
2016            &format!("/v1/domain/_attr/{ATTR_DOMAIN_LDAP_BASEDN}"),
2017            vec![new_basedn],
2018        )
2019        .await
2020    }
2021
2022    /// Sets the maximum number of LDAP attributes that can be queryed in a single operation
2023    pub async fn idm_domain_set_ldap_max_queryable_attrs(
2024        &self,
2025        max_queryable_attrs: usize,
2026    ) -> Result<(), ClientError> {
2027        self.perform_put_request(
2028            &format!("/v1/domain/_attr/{ATTR_LDAP_MAX_QUERYABLE_ATTRS}"),
2029            vec![max_queryable_attrs.to_string()],
2030        )
2031        .await
2032    }
2033
2034    pub async fn idm_set_ldap_allow_unix_password_bind(
2035        &self,
2036        enable: bool,
2037    ) -> Result<(), ClientError> {
2038        self.perform_put_request(
2039            &format!("{}{}", "/v1/domain/_attr/", ATTR_LDAP_ALLOW_UNIX_PW_BIND),
2040            vec![enable.to_string()],
2041        )
2042        .await
2043    }
2044
2045    pub async fn idm_domain_get_ssid(&self) -> Result<String, ClientError> {
2046        self.perform_get_request(&format!("/v1/domain/_attr/{ATTR_DOMAIN_SSID}"))
2047            .await
2048            .and_then(|mut r: Vec<String>|
2049                // Get the first result
2050                r.pop()
2051                .ok_or(
2052                    ClientError::EmptyResponse
2053                ))
2054    }
2055
2056    pub async fn idm_domain_set_ssid(&self, ssid: &str) -> Result<(), ClientError> {
2057        self.perform_put_request(
2058            &format!("/v1/domain/_attr/{ATTR_DOMAIN_SSID}"),
2059            vec![ssid.to_string()],
2060        )
2061        .await
2062    }
2063
2064    pub async fn idm_domain_revoke_key(&self, key_id: &str) -> Result<(), ClientError> {
2065        self.perform_put_request(
2066            &format!("/v1/domain/_attr/{ATTR_KEY_ACTION_REVOKE}"),
2067            vec![key_id.to_string()],
2068        )
2069        .await
2070    }
2071
2072    // ==== schema
2073    pub async fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
2074        self.perform_get_request("/v1/schema").await
2075    }
2076
2077    pub async fn idm_schema_attributetype_list(&self) -> Result<Vec<Entry>, ClientError> {
2078        self.perform_get_request("/v1/schema/attributetype").await
2079    }
2080
2081    pub async fn idm_schema_attributetype_get(
2082        &self,
2083        id: &str,
2084    ) -> Result<Option<Entry>, ClientError> {
2085        self.perform_get_request(&format!("/v1/schema/attributetype/{id}"))
2086            .await
2087    }
2088
2089    pub async fn idm_schema_classtype_list(&self) -> Result<Vec<Entry>, ClientError> {
2090        self.perform_get_request("/v1/schema/classtype").await
2091    }
2092
2093    pub async fn idm_schema_classtype_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
2094        self.perform_get_request(&format!("/v1/schema/classtype/{id}"))
2095            .await
2096    }
2097
2098    // ==== recycle bin
2099    pub async fn recycle_bin_list(&self) -> Result<Vec<Entry>, ClientError> {
2100        self.perform_get_request("/v1/recycle_bin").await
2101    }
2102
2103    pub async fn recycle_bin_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
2104        self.perform_get_request(&format!("/v1/recycle_bin/{id}"))
2105            .await
2106    }
2107
2108    pub async fn recycle_bin_revive(&self, id: &str) -> Result<(), ClientError> {
2109        self.perform_post_request(&format!("/v1/recycle_bin/{id}/_revive"), ())
2110            .await
2111    }
2112}
2113
2114#[cfg(test)]
2115mod tests {
2116    use super::{KanidmClient, KanidmClientBuilder};
2117    use kanidm_proto::constants::CLIENT_TOKEN_CACHE;
2118    use reqwest::StatusCode;
2119    use url::Url;
2120
2121    #[tokio::test]
2122    async fn test_no_client_version_check_on_502() {
2123        let res = reqwest::Response::from(
2124            http::Response::builder()
2125                .status(StatusCode::GATEWAY_TIMEOUT)
2126                .body("")
2127                .unwrap(),
2128        );
2129        let client = KanidmClientBuilder::new()
2130            .address("http://localhost:8080".to_string())
2131            .enable_native_ca_roots(false)
2132            .build()
2133            .expect("Failed to build client");
2134        eprintln!("This should pass because we are returning 504 and shouldn't check version...");
2135        client.expect_version(&res).await;
2136
2137        let res = reqwest::Response::from(
2138            http::Response::builder()
2139                .status(StatusCode::BAD_GATEWAY)
2140                .body("")
2141                .unwrap(),
2142        );
2143        let client = KanidmClientBuilder::new()
2144            .address("http://localhost:8080".to_string())
2145            .enable_native_ca_roots(false)
2146            .build()
2147            .expect("Failed to build client");
2148        eprintln!("This should pass because we are returning 502 and shouldn't check version...");
2149        client.expect_version(&res).await;
2150    }
2151
2152    #[test]
2153    fn test_make_url() {
2154        use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS;
2155        let client: KanidmClient = KanidmClientBuilder::new()
2156            .address(format!("https://{DEFAULT_SERVER_ADDRESS}"))
2157            .enable_native_ca_roots(false)
2158            .build()
2159            .unwrap();
2160        assert_eq!(
2161            client.get_url(),
2162            Url::parse(&format!("https://{DEFAULT_SERVER_ADDRESS}")).unwrap()
2163        );
2164        assert_eq!(
2165            client.make_url("/hello"),
2166            Url::parse(&format!("https://{DEFAULT_SERVER_ADDRESS}/hello")).unwrap()
2167        );
2168
2169        let client: KanidmClient = KanidmClientBuilder::new()
2170            .address(format!("https://{DEFAULT_SERVER_ADDRESS}/cheese/"))
2171            .enable_native_ca_roots(false)
2172            .build()
2173            .unwrap();
2174        assert_eq!(
2175            client.make_url("hello"),
2176            Url::parse(&format!("https://{DEFAULT_SERVER_ADDRESS}/cheese/hello")).unwrap()
2177        );
2178    }
2179
2180    #[test]
2181    fn test_kanidmclientbuilder_display() {
2182        let defaultclient = KanidmClientBuilder::default();
2183        println!("{defaultclient}");
2184        assert!(defaultclient.to_string().contains("verify_ca"));
2185
2186        let testclient = KanidmClientBuilder {
2187            address: Some("https://example.com".to_string()),
2188            verify_ca: true,
2189            verify_hostnames: true,
2190            ca: None,
2191            connect_timeout: Some(420),
2192            request_timeout: Some(69),
2193            use_system_proxies: true,
2194            token_cache_path: Some(CLIENT_TOKEN_CACHE.to_string()),
2195            disable_system_ca_store: false,
2196        };
2197        println!("testclient {testclient}");
2198        assert!(testclient.to_string().contains("verify_ca: true"));
2199        assert!(testclient.to_string().contains("verify_hostnames: true"));
2200
2201        let badness = testclient.danger_accept_invalid_hostnames(true);
2202        let badness = badness.danger_accept_invalid_certs(true);
2203        println!("badness: {badness}");
2204        assert!(badness.to_string().contains("verify_ca: false"));
2205        assert!(badness.to_string().contains("verify_hostnames: false"));
2206    }
2207}