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