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 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            .tls_built_in_native_certs(!self.disable_system_ca_store)
525            .danger_accept_invalid_hostnames(!self.verify_hostnames)
526            .danger_accept_invalid_certs(!self.verify_ca);
527
528        let client_builder = match self.use_system_proxies {
529            true => client_builder,
530            false => client_builder.no_proxy(),
531        };
532
533        let client_builder = match &self.ca {
534            Some(cert) => client_builder.add_root_certificate(cert.clone()),
535            None => client_builder,
536        };
537
538        let client_builder = match &self.connect_timeout {
539            Some(secs) => client_builder.connect_timeout(Duration::from_secs(*secs)),
540            None => client_builder,
541        };
542
543        let client_builder = match &self.request_timeout {
544            Some(secs) => client_builder.timeout(Duration::from_secs(*secs)),
545            None => client_builder,
546        };
547
548        let client = client_builder.build().map_err(ClientError::Transport)?;
549
550        // Now get the origin.
551        #[allow(clippy::expect_used)]
552        let uri = Url::parse(&address).expect("failed to parse address");
553
554        #[allow(clippy::expect_used)]
555        let origin =
556            Url::parse(&uri.origin().ascii_serialization()).expect("failed to parse origin");
557
558        let token_cache_path = match self.token_cache_path.clone() {
559            Some(val) => val.to_string(),
560            None => CLIENT_TOKEN_CACHE.to_string(),
561        };
562
563        Ok(KanidmClient {
564            client,
565            client_cookies,
566            addr: address,
567            builder: self,
568            bearer_token: RwLock::new(None),
569            auth_session_id: RwLock::new(None),
570            origin,
571            check_version: Mutex::new(true),
572            token_cache_path,
573        })
574    }
575}
576
577/// This is probably pretty jank but it works and was pulled from here:
578/// <https://github.com/seanmonstar/reqwest/issues/1602#issuecomment-1220996681>
579fn find_reqwest_error_source<E: std::error::Error + 'static>(
580    orig: &dyn std::error::Error,
581) -> Option<&E> {
582    let mut cause = orig.source();
583    while let Some(err) = cause {
584        if let Some(typed) = err.downcast_ref::<E>() {
585            return Some(typed);
586        }
587        cause = err.source();
588    }
589
590    // else
591    None
592}
593
594impl KanidmClient {
595    /// Access the underlying reqwest client that has been configured for this Kanidm server
596    pub fn client(&self) -> &reqwest::Client {
597        &self.client
598    }
599
600    pub fn get_origin(&self) -> &Url {
601        &self.origin
602    }
603
604    /// Returns the base URL of the server
605    pub fn get_url(&self) -> Url {
606        #[allow(clippy::panic)]
607        match self.addr.parse::<Url>() {
608            Ok(val) => val,
609            Err(err) => panic!("Failed to parse {} into URL: {:?}", self.addr, err),
610        }
611    }
612
613    /// Get a URL based on adding an endpoint to the base URL of the server
614    pub fn make_url(&self, endpoint: &str) -> Url {
615        #[allow(clippy::expect_used)]
616        self.get_url().join(endpoint).expect("Failed to join URL")
617    }
618
619    pub async fn set_token(&self, new_token: String) {
620        let mut tguard = self.bearer_token.write().await;
621        *tguard = Some(new_token);
622    }
623
624    pub async fn get_token(&self) -> Option<String> {
625        let tguard = self.bearer_token.read().await;
626        (*tguard).as_ref().cloned()
627    }
628
629    pub fn new_session(&self) -> Result<Self, ClientError> {
630        // Copy our builder, and then just process it.
631        let builder = self.builder.clone();
632        builder.build()
633    }
634
635    pub async fn logout(&self) -> Result<(), ClientError> {
636        match self.perform_get_request("/v1/logout").await {
637            Err(ClientError::Unauthorized)
638            | Err(ClientError::Http(reqwest::StatusCode::UNAUTHORIZED, _, _))
639            | Ok(()) => {
640                let mut tguard = self.bearer_token.write().await;
641                *tguard = None;
642                Ok(())
643            }
644            e => e,
645        }
646    }
647
648    pub fn get_token_cache_path(&self) -> String {
649        self.token_cache_path.clone()
650    }
651
652    /// Check that we're getting the right version back from the server.
653    async fn expect_version(&self, response: &reqwest::Response) {
654        let mut guard = self.check_version.lock().await;
655
656        if !*guard {
657            return;
658        }
659
660        if response.status() == StatusCode::BAD_GATEWAY
661            || response.status() == StatusCode::GATEWAY_TIMEOUT
662        {
663            // don't need to check versions when there's an intermediary reporting connectivity
664            debug!("Gateway error in response - we're going through a proxy so the version check is skipped.");
665            *guard = false;
666            return;
667        }
668
669        let ver: &str = response
670            .headers()
671            .get(KVERSION)
672            .and_then(|hv| hv.to_str().ok())
673            .unwrap_or("");
674
675        let matching = ver == EXPECT_VERSION;
676
677        if !matching {
678            warn!(server_version = ?ver, client_version = ?EXPECT_VERSION, "Mismatched client and server version - features may not work, or other unforeseen errors may occur.")
679        }
680
681        #[cfg(any(test, debug_assertions))]
682        if !matching && std::env::var("KANIDM_DEV_YOLO").is_err() {
683            eprintln!("⚠️  You're in debug/dev mode, so we're going to quit here.");
684            eprintln!("If you really must do this, set KANIDM_DEV_YOLO=1");
685            std::process::exit(1);
686        }
687
688        // Check is done once, mark as no longer needing to occur
689        *guard = false;
690    }
691
692    /// You've got the response from a reqwest and you want to turn it into a `ClientError`
693    pub fn handle_response_error(&self, error: reqwest::Error) -> ClientError {
694        if error.is_connect() {
695            if find_reqwest_error_source::<std::io::Error>(&error).is_some() {
696                // TODO: one day handle IO errors better
697                trace!("Got an IO error! {:?}", &error);
698                return ClientError::Transport(error);
699            }
700            if let Some(hyper_error) = find_reqwest_error_source::<hyper::Error>(&error) {
701                // hyper errors can be *anything* depending on the underlying client libraries
702                // ref: https://github.com/hyperium/hyper/blob/9feb70e9249d9fb99634ec96f83566e6bb3b3128/src/error.rs#L26C2-L26C2
703                if format!("{hyper_error:?}")
704                    .to_lowercase()
705                    .contains("certificate")
706                {
707                    return ClientError::UntrustedCertificate(format!("{hyper_error}"));
708                }
709            }
710        }
711        ClientError::Transport(error)
712    }
713
714    fn get_kopid_from_response(&self, response: &Response) -> String {
715        let opid = response
716            .headers()
717            .get(KOPID)
718            .and_then(|hv| hv.to_str().ok())
719            .unwrap_or("missing_kopid")
720            .to_string();
721
722        debug!("opid -> {:?}", opid);
723        opid
724    }
725
726    async fn perform_simple_post_request<R: Serialize, T: DeserializeOwned>(
727        &self,
728        dest: &str,
729        request: &R,
730    ) -> Result<T, ClientError> {
731        let response = self.client.post(self.make_url(dest)).json(request);
732
733        let response = response
734            .send()
735            .await
736            .map_err(|err| self.handle_response_error(err))?;
737
738        self.expect_version(&response).await;
739
740        let opid = self.get_kopid_from_response(&response);
741
742        self.ok_or_clienterror(&opid, response)
743            .await?
744            .json()
745            .await
746            .map_err(|e| ClientError::JsonDecode(e, opid))
747    }
748
749    async fn perform_auth_post_request<R: Serialize, T: DeserializeOwned>(
750        &self,
751        dest: &str,
752        request: R,
753    ) -> Result<T, ClientError> {
754        trace!("perform_auth_post_request connecting to {}", dest);
755
756        let auth_url = self.make_url(dest);
757
758        let response = self.client.post(auth_url.clone()).json(&request);
759
760        // If we have a bearer token, set it now.
761        let response = {
762            let tguard = self.bearer_token.read().await;
763            if let Some(token) = &(*tguard) {
764                response.bearer_auth(token)
765            } else {
766                response
767            }
768        };
769
770        // If we have a session header, set it now. This is only used when connecting
771        // to an older server.
772        let response = {
773            let sguard = self.auth_session_id.read().await;
774            if let Some(sessionid) = &(*sguard) {
775                response.header(KSESSIONID, sessionid)
776            } else {
777                response
778            }
779        };
780
781        let response = response
782            .send()
783            .await
784            .map_err(|err| self.handle_response_error(err))?;
785
786        self.expect_version(&response).await;
787
788        // If we have a sessionid header in the response, get it now.
789        let opid = self.get_kopid_from_response(&response);
790
791        let response = self.ok_or_clienterror(&opid, response).await?;
792
793        // Do we have a cookie? Our job here isn't to parse and validate the cookies, but just to
794        // know if the session id was set *in* our cookie store at all.
795        let cookie_present = self
796            .client_cookies
797            .cookies(&auth_url)
798            .map(|cookie_header| {
799                cookie_header
800                    .to_str()
801                    .ok()
802                    .map(|cookie_str| {
803                        cookie_str
804                            .split(';')
805                            .filter_map(|c| c.split_once('='))
806                            .any(|(name, _)| name == COOKIE_AUTH_SESSION_ID)
807                    })
808                    .unwrap_or_default()
809            })
810            .unwrap_or_default();
811
812        {
813            let headers = response.headers();
814
815            let mut sguard = self.auth_session_id.write().await;
816            trace!(?cookie_present);
817            if cookie_present {
818                // Clear and auth session id if present, we have the cookie instead.
819                *sguard = None;
820            } else {
821                // This situation occurs when a newer client connects to an older server
822                debug!("Auth SessionID cookie not present, falling back to header.");
823                *sguard = headers
824                    .get(KSESSIONID)
825                    .and_then(|hv| hv.to_str().ok().map(str::to_string));
826            }
827        }
828
829        response
830            .json()
831            .await
832            .map_err(|e| ClientError::JsonDecode(e, opid))
833    }
834
835    pub async fn perform_post_request<R: Serialize, T: DeserializeOwned>(
836        &self,
837        dest: &str,
838        request: R,
839    ) -> Result<T, ClientError> {
840        let response = self.client.post(self.make_url(dest)).json(&request);
841
842        let response = {
843            let tguard = self.bearer_token.read().await;
844            if let Some(token) = &(*tguard) {
845                response.bearer_auth(token)
846            } else {
847                response
848            }
849        };
850
851        let response = response
852            .send()
853            .await
854            .map_err(|err| self.handle_response_error(err))?;
855
856        self.expect_version(&response).await;
857
858        let opid = self.get_kopid_from_response(&response);
859
860        self.ok_or_clienterror(&opid, response)
861            .await?
862            .json()
863            .await
864            .map_err(|e| ClientError::JsonDecode(e, opid))
865    }
866
867    async fn perform_put_request<R: Serialize, T: DeserializeOwned>(
868        &self,
869        dest: &str,
870        request: R,
871    ) -> Result<T, ClientError> {
872        let response = self.client.put(self.make_url(dest)).json(&request);
873
874        let response = {
875            let tguard = self.bearer_token.read().await;
876            if let Some(token) = &(*tguard) {
877                response.bearer_auth(token)
878            } else {
879                response
880            }
881        };
882
883        let response = response
884            .send()
885            .await
886            .map_err(|err| self.handle_response_error(err))?;
887
888        self.expect_version(&response).await;
889
890        let opid = self.get_kopid_from_response(&response);
891
892        match response.status() {
893            reqwest::StatusCode::OK => {}
894            reqwest::StatusCode::UNPROCESSABLE_ENTITY => {
895                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() )))
896            }
897
898            unexpected => {
899                return Err(ClientError::Http(
900                    unexpected,
901                    response.json().await.ok(),
902                    opid,
903                ))
904            }
905        }
906
907        response
908            .json()
909            .await
910            .map_err(|e| ClientError::JsonDecode(e, opid))
911    }
912
913    /// Handles not-OK responses and turns them into ClientErrors
914    async fn ok_or_clienterror(
915        &self,
916        opid: &str,
917        response: reqwest::Response,
918    ) -> Result<reqwest::Response, ClientError> {
919        match response.status() {
920            reqwest::StatusCode::OK => Ok(response),
921            unexpected => Err(ClientError::Http(
922                unexpected,
923                response.json().await.ok(),
924                opid.to_string(),
925            )),
926        }
927    }
928
929    pub async fn perform_patch_request<R: Serialize, T: DeserializeOwned>(
930        &self,
931        dest: &str,
932        request: R,
933    ) -> Result<T, ClientError> {
934        let response = self.client.patch(self.make_url(dest)).json(&request);
935
936        let response = {
937            let tguard = self.bearer_token.read().await;
938            if let Some(token) = &(*tguard) {
939                response.bearer_auth(token)
940            } else {
941                response
942            }
943        };
944
945        let response = response
946            .send()
947            .await
948            .map_err(|err| self.handle_response_error(err))?;
949
950        self.expect_version(&response).await;
951
952        let opid = self.get_kopid_from_response(&response);
953
954        self.ok_or_clienterror(&opid, response)
955            .await?
956            .json()
957            .await
958            .map_err(|e| ClientError::JsonDecode(e, opid))
959    }
960
961    #[instrument(level = "debug", skip(self))]
962    pub async fn perform_get_request<T: DeserializeOwned>(
963        &self,
964        dest: &str,
965    ) -> Result<T, ClientError> {
966        let query: Option<()> = None;
967        self.perform_get_request_query(dest, query).await
968    }
969
970    #[instrument(level = "debug", skip(self))]
971    pub async fn perform_get_request_query<T: DeserializeOwned, Q: Serialize + Debug>(
972        &self,
973        dest: &str,
974        query: Option<Q>,
975    ) -> Result<T, ClientError> {
976        let mut dest_url = self.make_url(dest);
977
978        if let Some(query) = query {
979            let txt = serde_urlencoded::to_string(&query).map_err(ClientError::UrlEncode)?;
980
981            if !txt.is_empty() {
982                dest_url.set_query(Some(txt.as_str()));
983            }
984        }
985
986        let response = self.client.get(dest_url);
987        let response = {
988            let tguard = self.bearer_token.read().await;
989            if let Some(token) = &(*tguard) {
990                response.bearer_auth(token)
991            } else {
992                response
993            }
994        };
995
996        let response = response
997            .send()
998            .await
999            .map_err(|err| self.handle_response_error(err))?;
1000
1001        self.expect_version(&response).await;
1002
1003        let opid = self.get_kopid_from_response(&response);
1004
1005        self.ok_or_clienterror(&opid, response)
1006            .await?
1007            .json()
1008            .await
1009            .map_err(|e| ClientError::JsonDecode(e, opid))
1010    }
1011
1012    async fn perform_delete_request(&self, dest: &str) -> Result<(), ClientError> {
1013        let response = self
1014            .client
1015            .delete(self.make_url(dest))
1016            // empty-ish body that makes the parser happy
1017            .json(&serde_json::json!([]));
1018
1019        let response = {
1020            let tguard = self.bearer_token.read().await;
1021            if let Some(token) = &(*tguard) {
1022                response.bearer_auth(token)
1023            } else {
1024                response
1025            }
1026        };
1027
1028        let response = response
1029            .send()
1030            .await
1031            .map_err(|err| self.handle_response_error(err))?;
1032
1033        self.expect_version(&response).await;
1034
1035        let opid = self.get_kopid_from_response(&response);
1036
1037        self.ok_or_clienterror(&opid, response)
1038            .await?
1039            .json()
1040            .await
1041            .map_err(|e| ClientError::JsonDecode(e, opid))
1042    }
1043
1044    async fn perform_delete_request_with_body<R: Serialize>(
1045        &self,
1046        dest: &str,
1047        request: R,
1048    ) -> Result<(), ClientError> {
1049        let response = self.client.delete(self.make_url(dest)).json(&request);
1050
1051        let response = {
1052            let tguard = self.bearer_token.read().await;
1053            if let Some(token) = &(*tguard) {
1054                response.bearer_auth(token)
1055            } else {
1056                response
1057            }
1058        };
1059
1060        let response = response
1061            .send()
1062            .await
1063            .map_err(|err| self.handle_response_error(err))?;
1064
1065        self.expect_version(&response).await;
1066
1067        let opid = self.get_kopid_from_response(&response);
1068
1069        self.ok_or_clienterror(&opid, response)
1070            .await?
1071            .json()
1072            .await
1073            .map_err(|e| ClientError::JsonDecode(e, opid))
1074    }
1075
1076    #[instrument(level = "debug", skip(self))]
1077    pub async fn auth_step_init(&self, ident: &str) -> Result<Set<AuthMech>, ClientError> {
1078        let auth_init = AuthRequest {
1079            step: AuthStep::Init2 {
1080                username: ident.to_string(),
1081                issue: AuthIssueSession::Token,
1082                privileged: false,
1083            },
1084        };
1085
1086        let r: Result<AuthResponse, _> =
1087            self.perform_auth_post_request("/v1/auth", auth_init).await;
1088        r.map(|v| {
1089            debug!("Authentication Session ID -> {:?}", v.sessionid);
1090            // Stash the session ID header.
1091            v.state
1092        })
1093        .and_then(|state| match state {
1094            AuthState::Choose(mechs) => Ok(mechs),
1095            _ => Err(ClientError::AuthenticationFailed),
1096        })
1097        .map(|mechs| mechs.into_iter().collect())
1098    }
1099
1100    #[instrument(level = "debug", skip(self))]
1101    pub async fn auth_step_begin(&self, mech: AuthMech) -> Result<Vec<AuthAllowed>, ClientError> {
1102        let auth_begin = AuthRequest {
1103            step: AuthStep::Begin(mech),
1104        };
1105
1106        let r: Result<AuthResponse, _> =
1107            self.perform_auth_post_request("/v1/auth", auth_begin).await;
1108        r.map(|v| {
1109            debug!("Authentication Session ID -> {:?}", v.sessionid);
1110            v.state
1111        })
1112        .and_then(|state| match state {
1113            AuthState::Continue(allowed) => Ok(allowed),
1114            _ => Err(ClientError::AuthenticationFailed),
1115        })
1116        // For converting to a Set
1117        // .map(|allowed| allowed.into_iter().collect())
1118    }
1119
1120    #[instrument(level = "debug", skip_all)]
1121    pub async fn auth_step_anonymous(&self) -> Result<AuthResponse, ClientError> {
1122        let auth_anon = AuthRequest {
1123            step: AuthStep::Cred(AuthCredential::Anonymous),
1124        };
1125        let r: Result<AuthResponse, _> =
1126            self.perform_auth_post_request("/v1/auth", auth_anon).await;
1127
1128        if let Ok(ar) = &r {
1129            if let AuthState::Success(token) = &ar.state {
1130                self.set_token(token.clone()).await;
1131            };
1132        };
1133        r
1134    }
1135
1136    #[instrument(level = "debug", skip_all)]
1137    pub async fn auth_step_password(&self, password: &str) -> Result<AuthResponse, ClientError> {
1138        let auth_req = AuthRequest {
1139            step: AuthStep::Cred(AuthCredential::Password(password.to_string())),
1140        };
1141        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1142
1143        if let Ok(ar) = &r {
1144            if let AuthState::Success(token) = &ar.state {
1145                self.set_token(token.clone()).await;
1146            };
1147        };
1148        r
1149    }
1150
1151    #[instrument(level = "debug", skip_all)]
1152    pub async fn auth_step_backup_code(
1153        &self,
1154        backup_code: &str,
1155    ) -> Result<AuthResponse, ClientError> {
1156        let auth_req = AuthRequest {
1157            step: AuthStep::Cred(AuthCredential::BackupCode(backup_code.to_string())),
1158        };
1159        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1160
1161        if let Ok(ar) = &r {
1162            if let AuthState::Success(token) = &ar.state {
1163                self.set_token(token.clone()).await;
1164            };
1165        };
1166        r
1167    }
1168
1169    #[instrument(level = "debug", skip_all)]
1170    pub async fn auth_step_totp(&self, totp: u32) -> Result<AuthResponse, ClientError> {
1171        let auth_req = AuthRequest {
1172            step: AuthStep::Cred(AuthCredential::Totp(totp)),
1173        };
1174        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1175
1176        if let Ok(ar) = &r {
1177            if let AuthState::Success(token) = &ar.state {
1178                self.set_token(token.clone()).await;
1179            };
1180        };
1181        r
1182    }
1183
1184    #[instrument(level = "debug", skip_all)]
1185    pub async fn auth_step_securitykey_complete(
1186        &self,
1187        pkc: Box<PublicKeyCredential>,
1188    ) -> Result<AuthResponse, ClientError> {
1189        let auth_req = AuthRequest {
1190            step: AuthStep::Cred(AuthCredential::SecurityKey(pkc)),
1191        };
1192        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1193
1194        if let Ok(ar) = &r {
1195            if let AuthState::Success(token) = &ar.state {
1196                self.set_token(token.clone()).await;
1197            };
1198        };
1199        r
1200    }
1201
1202    #[instrument(level = "debug", skip_all)]
1203    pub async fn auth_step_passkey_complete(
1204        &self,
1205        pkc: Box<PublicKeyCredential>,
1206    ) -> Result<AuthResponse, ClientError> {
1207        let auth_req = AuthRequest {
1208            step: AuthStep::Cred(AuthCredential::Passkey(pkc)),
1209        };
1210        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1211
1212        if let Ok(ar) = &r {
1213            if let AuthState::Success(token) = &ar.state {
1214                self.set_token(token.clone()).await;
1215            };
1216        };
1217        r
1218    }
1219
1220    #[instrument(level = "debug", skip(self))]
1221    pub async fn auth_anonymous(&self) -> Result<(), ClientError> {
1222        let mechs = match self.auth_step_init("anonymous").await {
1223            Ok(s) => s,
1224            Err(e) => return Err(e),
1225        };
1226
1227        if !mechs.contains(&AuthMech::Anonymous) {
1228            debug!("Anonymous mech not presented");
1229            return Err(ClientError::AuthenticationFailed);
1230        }
1231
1232        let _state = match self.auth_step_begin(AuthMech::Anonymous).await {
1233            Ok(s) => s,
1234            Err(e) => return Err(e),
1235        };
1236
1237        let r = self.auth_step_anonymous().await?;
1238
1239        match r.state {
1240            AuthState::Success(token) => {
1241                self.set_token(token.clone()).await;
1242                Ok(())
1243            }
1244            _ => Err(ClientError::AuthenticationFailed),
1245        }
1246    }
1247
1248    #[instrument(level = "debug", skip(self, password))]
1249    pub async fn auth_simple_password(
1250        &self,
1251        ident: &str,
1252        password: &str,
1253    ) -> Result<(), ClientError> {
1254        trace!("Init auth step");
1255        let mechs = match self.auth_step_init(ident).await {
1256            Ok(s) => s,
1257            Err(e) => return Err(e),
1258        };
1259
1260        if !mechs.contains(&AuthMech::Password) {
1261            debug!("Password mech not presented");
1262            return Err(ClientError::AuthenticationFailed);
1263        }
1264
1265        let _state = match self.auth_step_begin(AuthMech::Password).await {
1266            Ok(s) => s,
1267            Err(e) => return Err(e),
1268        };
1269
1270        let r = self.auth_step_password(password).await?;
1271
1272        match r.state {
1273            AuthState::Success(_) => Ok(()),
1274            _ => Err(ClientError::AuthenticationFailed),
1275        }
1276    }
1277
1278    #[instrument(level = "debug", skip(self, password, totp))]
1279    pub async fn auth_password_totp(
1280        &self,
1281        ident: &str,
1282        password: &str,
1283        totp: u32,
1284    ) -> Result<(), ClientError> {
1285        let mechs = match self.auth_step_init(ident).await {
1286            Ok(s) => s,
1287            Err(e) => return Err(e),
1288        };
1289
1290        if !mechs.contains(&AuthMech::PasswordTotp) {
1291            debug!("PasswordTotp mech not presented");
1292            return Err(ClientError::AuthenticationFailed);
1293        }
1294
1295        let state = match self.auth_step_begin(AuthMech::PasswordTotp).await {
1296            Ok(s) => s,
1297            Err(e) => return Err(e),
1298        };
1299
1300        if !state.contains(&AuthAllowed::Totp) {
1301            debug!("TOTP step not offered.");
1302            return Err(ClientError::AuthenticationFailed);
1303        }
1304
1305        let r = self.auth_step_totp(totp).await?;
1306
1307        // Should need to continue.
1308        match r.state {
1309            AuthState::Continue(allowed) => {
1310                if !allowed.contains(&AuthAllowed::Password) {
1311                    debug!("Password step not offered.");
1312                    return Err(ClientError::AuthenticationFailed);
1313                }
1314            }
1315            _ => {
1316                debug!("Invalid AuthState presented.");
1317                return Err(ClientError::AuthenticationFailed);
1318            }
1319        };
1320
1321        let r = self.auth_step_password(password).await?;
1322
1323        match r.state {
1324            AuthState::Success(_token) => Ok(()),
1325            _ => Err(ClientError::AuthenticationFailed),
1326        }
1327    }
1328
1329    #[instrument(level = "debug", skip(self, password, backup_code))]
1330    pub async fn auth_password_backup_code(
1331        &self,
1332        ident: &str,
1333        password: &str,
1334        backup_code: &str,
1335    ) -> Result<(), ClientError> {
1336        let mechs = match self.auth_step_init(ident).await {
1337            Ok(s) => s,
1338            Err(e) => return Err(e),
1339        };
1340
1341        if !mechs.contains(&AuthMech::PasswordBackupCode) {
1342            debug!("PasswordBackupCode mech not presented");
1343            return Err(ClientError::AuthenticationFailed);
1344        }
1345
1346        let state = match self.auth_step_begin(AuthMech::PasswordBackupCode).await {
1347            Ok(s) => s,
1348            Err(e) => return Err(e),
1349        };
1350
1351        if !state.contains(&AuthAllowed::BackupCode) {
1352            debug!("Backup Code step not offered.");
1353            return Err(ClientError::AuthenticationFailed);
1354        }
1355
1356        let r = self.auth_step_backup_code(backup_code).await?;
1357
1358        // Should need to continue.
1359        match r.state {
1360            AuthState::Continue(allowed) => {
1361                if !allowed.contains(&AuthAllowed::Password) {
1362                    debug!("Password step not offered.");
1363                    return Err(ClientError::AuthenticationFailed);
1364                }
1365            }
1366            _ => {
1367                debug!("Invalid AuthState presented.");
1368                return Err(ClientError::AuthenticationFailed);
1369            }
1370        };
1371
1372        let r = self.auth_step_password(password).await?;
1373
1374        match r.state {
1375            AuthState::Success(_token) => Ok(()),
1376            _ => Err(ClientError::AuthenticationFailed),
1377        }
1378    }
1379
1380    #[instrument(level = "debug", skip(self))]
1381    pub async fn auth_passkey_begin(
1382        &self,
1383        ident: &str,
1384    ) -> Result<RequestChallengeResponse, ClientError> {
1385        let mechs = match self.auth_step_init(ident).await {
1386            Ok(s) => s,
1387            Err(e) => return Err(e),
1388        };
1389
1390        if !mechs.contains(&AuthMech::Passkey) {
1391            debug!("Webauthn mech not presented");
1392            return Err(ClientError::AuthenticationFailed);
1393        }
1394
1395        let state = match self.auth_step_begin(AuthMech::Passkey).await {
1396            Ok(mut s) => s.pop(),
1397            Err(e) => return Err(e),
1398        };
1399
1400        // State is now a set of auth continues.
1401        match state {
1402            Some(AuthAllowed::Passkey(r)) => Ok(r),
1403            _ => Err(ClientError::AuthenticationFailed),
1404        }
1405    }
1406
1407    #[instrument(level = "debug", skip_all)]
1408    pub async fn auth_passkey_complete(
1409        &self,
1410        pkc: Box<PublicKeyCredential>,
1411    ) -> Result<(), ClientError> {
1412        let r = self.auth_step_passkey_complete(pkc).await?;
1413        match r.state {
1414            AuthState::Success(_token) => Ok(()),
1415            _ => Err(ClientError::AuthenticationFailed),
1416        }
1417    }
1418
1419    pub async fn reauth_begin(&self) -> Result<Vec<AuthAllowed>, ClientError> {
1420        let issue = AuthIssueSession::Token;
1421        let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/reauth", issue).await;
1422
1423        r.map(|v| {
1424            debug!("Authentication Session ID -> {:?}", v.sessionid);
1425            v.state
1426        })
1427        .and_then(|state| match state {
1428            AuthState::Continue(allowed) => Ok(allowed),
1429            _ => Err(ClientError::AuthenticationFailed),
1430        })
1431    }
1432
1433    #[instrument(level = "debug", skip_all)]
1434    pub async fn reauth_simple_password(&self, password: &str) -> Result<(), ClientError> {
1435        let state = match self.reauth_begin().await {
1436            Ok(mut s) => s.pop(),
1437            Err(e) => return Err(e),
1438        };
1439
1440        match state {
1441            Some(AuthAllowed::Password) => {}
1442            _ => {
1443                return Err(ClientError::AuthenticationFailed);
1444            }
1445        };
1446
1447        let r = self.auth_step_password(password).await?;
1448
1449        match r.state {
1450            AuthState::Success(_) => Ok(()),
1451            _ => Err(ClientError::AuthenticationFailed),
1452        }
1453    }
1454
1455    #[instrument(level = "debug", skip_all)]
1456    pub async fn reauth_password_totp(&self, password: &str, totp: u32) -> Result<(), ClientError> {
1457        let state = match self.reauth_begin().await {
1458            Ok(s) => s,
1459            Err(e) => return Err(e),
1460        };
1461
1462        if !state.contains(&AuthAllowed::Totp) {
1463            debug!("TOTP step not offered.");
1464            return Err(ClientError::AuthenticationFailed);
1465        }
1466
1467        let r = self.auth_step_totp(totp).await?;
1468
1469        // Should need to continue.
1470        match r.state {
1471            AuthState::Continue(allowed) => {
1472                if !allowed.contains(&AuthAllowed::Password) {
1473                    debug!("Password step not offered.");
1474                    return Err(ClientError::AuthenticationFailed);
1475                }
1476            }
1477            _ => {
1478                debug!("Invalid AuthState presented.");
1479                return Err(ClientError::AuthenticationFailed);
1480            }
1481        };
1482
1483        let r = self.auth_step_password(password).await?;
1484
1485        match r.state {
1486            AuthState::Success(_token) => Ok(()),
1487            _ => Err(ClientError::AuthenticationFailed),
1488        }
1489    }
1490
1491    #[instrument(level = "debug", skip_all)]
1492    pub async fn reauth_passkey_begin(&self) -> Result<RequestChallengeResponse, ClientError> {
1493        let state = match self.reauth_begin().await {
1494            Ok(mut s) => s.pop(),
1495            Err(e) => return Err(e),
1496        };
1497
1498        // State is now a set of auth continues.
1499        match state {
1500            Some(AuthAllowed::Passkey(r)) => Ok(r),
1501            _ => Err(ClientError::AuthenticationFailed),
1502        }
1503    }
1504
1505    #[instrument(level = "debug", skip_all)]
1506    pub async fn reauth_passkey_complete(
1507        &self,
1508        pkc: Box<PublicKeyCredential>,
1509    ) -> Result<(), ClientError> {
1510        let r = self.auth_step_passkey_complete(pkc).await?;
1511        match r.state {
1512            AuthState::Success(_token) => Ok(()),
1513            _ => Err(ClientError::AuthenticationFailed),
1514        }
1515    }
1516
1517    pub async fn auth_valid(&self) -> Result<(), ClientError> {
1518        self.perform_get_request(V1_AUTH_VALID).await
1519    }
1520
1521    pub async fn get_public_jwk(&self, key_id: &str) -> Result<Jwk, ClientError> {
1522        self.perform_get_request(&format!("/v1/jwk/{key_id}")).await
1523    }
1524
1525    pub async fn whoami(&self) -> Result<Option<Entry>, ClientError> {
1526        let response = self.client.get(self.make_url("/v1/self"));
1527
1528        let response = {
1529            let tguard = self.bearer_token.read().await;
1530            if let Some(token) = &(*tguard) {
1531                response.bearer_auth(token)
1532            } else {
1533                response
1534            }
1535        };
1536
1537        let response = response
1538            .send()
1539            .await
1540            .map_err(|err| self.handle_response_error(err))?;
1541
1542        self.expect_version(&response).await;
1543
1544        let opid = self.get_kopid_from_response(&response);
1545        match response.status() {
1546            // Continue to process.
1547            reqwest::StatusCode::OK => {}
1548            reqwest::StatusCode::UNAUTHORIZED => return Ok(None),
1549            unexpected => {
1550                return Err(ClientError::Http(
1551                    unexpected,
1552                    response.json().await.ok(),
1553                    opid,
1554                ))
1555            }
1556        }
1557
1558        let r: WhoamiResponse = response
1559            .json()
1560            .await
1561            .map_err(|e| ClientError::JsonDecode(e, opid))?;
1562
1563        Ok(Some(r.youare))
1564    }
1565
1566    // Raw DB actions
1567    pub async fn search(&self, filter: Filter) -> Result<Vec<Entry>, ClientError> {
1568        let sr = SearchRequest { filter };
1569        let r: Result<SearchResponse, _> = self.perform_post_request("/v1/raw/search", sr).await;
1570        r.map(|v| v.entries)
1571    }
1572
1573    pub async fn create(&self, entries: Vec<Entry>) -> Result<(), ClientError> {
1574        let c = CreateRequest { entries };
1575        self.perform_post_request("/v1/raw/create", c).await
1576    }
1577
1578    pub async fn modify(&self, filter: Filter, modlist: ModifyList) -> Result<(), ClientError> {
1579        let mr = ModifyRequest { filter, modlist };
1580        self.perform_post_request("/v1/raw/modify", mr).await
1581    }
1582
1583    pub async fn delete(&self, filter: Filter) -> Result<(), ClientError> {
1584        let dr = DeleteRequest { filter };
1585        self.perform_post_request("/v1/raw/delete", dr).await
1586    }
1587
1588    // === idm actions here ==
1589
1590    // ===== GROUPS
1591    pub async fn idm_group_list(&self) -> Result<Vec<Entry>, ClientError> {
1592        self.perform_get_request("/v1/group").await
1593    }
1594
1595    pub async fn idm_group_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
1596        self.perform_get_request(&format!("/v1/group/{id}")).await
1597    }
1598
1599    pub async fn idm_group_get_members(
1600        &self,
1601        id: &str,
1602    ) -> Result<Option<Vec<String>>, ClientError> {
1603        self.perform_get_request(&format!("/v1/group/{id}/_attr/member"))
1604            .await
1605    }
1606
1607    pub async fn idm_group_create(
1608        &self,
1609        name: &str,
1610        entry_managed_by: Option<&str>,
1611    ) -> Result<(), ClientError> {
1612        let mut new_group = Entry {
1613            attrs: BTreeMap::new(),
1614        };
1615        new_group
1616            .attrs
1617            .insert(ATTR_NAME.to_string(), vec![name.to_string()]);
1618
1619        if let Some(entry_manager) = entry_managed_by {
1620            new_group.attrs.insert(
1621                ATTR_ENTRY_MANAGED_BY.to_string(),
1622                vec![entry_manager.to_string()],
1623            );
1624        }
1625
1626        self.perform_post_request("/v1/group", new_group).await
1627    }
1628
1629    pub async fn idm_group_set_entry_managed_by(
1630        &self,
1631        id: &str,
1632        entry_manager: &str,
1633    ) -> Result<(), ClientError> {
1634        let data = vec![entry_manager];
1635        self.perform_put_request(&format!("/v1/group/{id}/_attr/entry_managed_by"), data)
1636            .await
1637    }
1638
1639    pub async fn idm_group_set_members(
1640        &self,
1641        id: &str,
1642        members: &[&str],
1643    ) -> Result<(), ClientError> {
1644        let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect();
1645        self.perform_put_request(&format!("/v1/group/{id}/_attr/member"), m)
1646            .await
1647    }
1648
1649    pub async fn idm_group_add_members(
1650        &self,
1651        id: &str,
1652        members: &[&str],
1653    ) -> Result<(), ClientError> {
1654        let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect();
1655        self.perform_post_request(&format!("/v1/group/{id}/_attr/member"), m)
1656            .await
1657    }
1658
1659    pub async fn idm_group_remove_members(
1660        &self,
1661        group: &str,
1662        members: &[&str],
1663    ) -> Result<(), ClientError> {
1664        debug!(
1665            "Asked to remove members {} from {}",
1666            &members.join(","),
1667            group
1668        );
1669        self.perform_delete_request_with_body(&format!("/v1/group/{group}/_attr/member"), &members)
1670            .await
1671    }
1672
1673    pub async fn idm_group_purge_members(&self, id: &str) -> Result<(), ClientError> {
1674        self.perform_delete_request(&format!("/v1/group/{id}/_attr/member"))
1675            .await
1676    }
1677
1678    pub async fn idm_group_unix_extend(
1679        &self,
1680        id: &str,
1681        gidnumber: Option<u32>,
1682    ) -> Result<(), ClientError> {
1683        let gx = GroupUnixExtend { gidnumber };
1684        self.perform_post_request(&format!("/v1/group/{id}/_unix"), gx)
1685            .await
1686    }
1687
1688    pub async fn idm_group_unix_token_get(&self, id: &str) -> Result<UnixGroupToken, ClientError> {
1689        self.perform_get_request(&format!("/v1/group/{id}/_unix/_token"))
1690            .await
1691    }
1692
1693    pub async fn idm_group_delete(&self, id: &str) -> Result<(), ClientError> {
1694        self.perform_delete_request(&format!("/v1/group/{id}"))
1695            .await
1696    }
1697
1698    // ==== ACCOUNTS
1699
1700    pub async fn idm_account_unix_token_get(&self, id: &str) -> Result<UnixUserToken, ClientError> {
1701        self.perform_get_request(&format!("/v1/account/{id}/_unix/_token"))
1702            .await
1703    }
1704
1705    // == new credential update session code.
1706    #[instrument(level = "debug", skip(self))]
1707    pub async fn idm_person_account_credential_update_intent(
1708        &self,
1709        id: &str,
1710        ttl: Option<u32>,
1711    ) -> Result<CUIntentToken, ClientError> {
1712        if let Some(ttl) = ttl {
1713            self.perform_get_request(&format!("/v1/person/{id}/_credential/_update_intent/{ttl}"))
1714                .await
1715        } else {
1716            self.perform_get_request(&format!("/v1/person/{id}/_credential/_update_intent"))
1717                .await
1718        }
1719    }
1720
1721    pub async fn idm_account_credential_update_begin(
1722        &self,
1723        id: &str,
1724    ) -> Result<(CUSessionToken, CUStatus), ClientError> {
1725        self.perform_get_request(&format!("/v1/person/{id}/_credential/_update"))
1726            .await
1727    }
1728
1729    pub async fn idm_account_credential_update_exchange(
1730        &self,
1731        intent_token: String,
1732    ) -> Result<(CUSessionToken, CUStatus), ClientError> {
1733        // We don't need to send the UAT with these, which is why we use the different path.
1734        self.perform_simple_post_request("/v1/credential/_exchange_intent", &intent_token)
1735            .await
1736    }
1737
1738    pub async fn idm_account_credential_update_status(
1739        &self,
1740        session_token: &CUSessionToken,
1741    ) -> Result<CUStatus, ClientError> {
1742        self.perform_simple_post_request("/v1/credential/_status", &session_token)
1743            .await
1744    }
1745
1746    pub async fn idm_account_credential_update_set_password(
1747        &self,
1748        session_token: &CUSessionToken,
1749        pw: &str,
1750    ) -> Result<CUStatus, ClientError> {
1751        let scr = CURequest::Password(pw.to_string());
1752        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1753            .await
1754    }
1755
1756    pub async fn idm_account_credential_update_cancel_mfareg(
1757        &self,
1758        session_token: &CUSessionToken,
1759    ) -> Result<CUStatus, ClientError> {
1760        let scr = CURequest::CancelMFAReg;
1761        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1762            .await
1763    }
1764
1765    pub async fn idm_account_credential_update_init_totp(
1766        &self,
1767        session_token: &CUSessionToken,
1768    ) -> Result<CUStatus, ClientError> {
1769        let scr = CURequest::TotpGenerate;
1770        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1771            .await
1772    }
1773
1774    pub async fn idm_account_credential_update_check_totp(
1775        &self,
1776        session_token: &CUSessionToken,
1777        totp_chal: u32,
1778        label: &str,
1779    ) -> Result<CUStatus, ClientError> {
1780        let scr = CURequest::TotpVerify(totp_chal, label.to_string());
1781        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1782            .await
1783    }
1784
1785    // TODO: add test coverage
1786    pub async fn idm_account_credential_update_accept_sha1_totp(
1787        &self,
1788        session_token: &CUSessionToken,
1789    ) -> Result<CUStatus, ClientError> {
1790        let scr = CURequest::TotpAcceptSha1;
1791        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1792            .await
1793    }
1794
1795    pub async fn idm_account_credential_update_remove_totp(
1796        &self,
1797        session_token: &CUSessionToken,
1798        label: &str,
1799    ) -> Result<CUStatus, ClientError> {
1800        let scr = CURequest::TotpRemove(label.to_string());
1801        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1802            .await
1803    }
1804
1805    // TODO: add test coverage
1806    pub async fn idm_account_credential_update_backup_codes_generate(
1807        &self,
1808        session_token: &CUSessionToken,
1809    ) -> Result<CUStatus, ClientError> {
1810        let scr = CURequest::BackupCodeGenerate;
1811        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1812            .await
1813    }
1814
1815    // TODO: add test coverage
1816    pub async fn idm_account_credential_update_primary_remove(
1817        &self,
1818        session_token: &CUSessionToken,
1819    ) -> Result<CUStatus, ClientError> {
1820        let scr = CURequest::PrimaryRemove;
1821        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1822            .await
1823    }
1824
1825    pub async fn idm_account_credential_update_set_unix_password(
1826        &self,
1827        session_token: &CUSessionToken,
1828        pw: &str,
1829    ) -> Result<CUStatus, ClientError> {
1830        let scr = CURequest::UnixPassword(pw.to_string());
1831        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1832            .await
1833    }
1834
1835    pub async fn idm_account_credential_update_unix_remove(
1836        &self,
1837        session_token: &CUSessionToken,
1838    ) -> Result<CUStatus, ClientError> {
1839        let scr = CURequest::UnixPasswordRemove;
1840        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1841            .await
1842    }
1843
1844    pub async fn idm_account_credential_update_sshkey_add(
1845        &self,
1846        session_token: &CUSessionToken,
1847        label: String,
1848        key: SshPublicKey,
1849    ) -> Result<CUStatus, ClientError> {
1850        let scr = CURequest::SshPublicKey(label, key);
1851        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1852            .await
1853    }
1854
1855    pub async fn idm_account_credential_update_sshkey_remove(
1856        &self,
1857        session_token: &CUSessionToken,
1858        label: String,
1859    ) -> Result<CUStatus, ClientError> {
1860        let scr = CURequest::SshPublicKeyRemove(label);
1861        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1862            .await
1863    }
1864
1865    pub async fn idm_account_credential_update_passkey_init(
1866        &self,
1867        session_token: &CUSessionToken,
1868    ) -> Result<CUStatus, ClientError> {
1869        let scr = CURequest::PasskeyInit;
1870        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1871            .await
1872    }
1873
1874    pub async fn idm_account_credential_update_passkey_finish(
1875        &self,
1876        session_token: &CUSessionToken,
1877        label: String,
1878        registration: RegisterPublicKeyCredential,
1879    ) -> Result<CUStatus, ClientError> {
1880        let scr = CURequest::PasskeyFinish(label, registration);
1881        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1882            .await
1883    }
1884
1885    // TODO: add test coverage
1886    pub async fn idm_account_credential_update_passkey_remove(
1887        &self,
1888        session_token: &CUSessionToken,
1889        uuid: Uuid,
1890    ) -> Result<CUStatus, ClientError> {
1891        let scr = CURequest::PasskeyRemove(uuid);
1892        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1893            .await
1894    }
1895
1896    pub async fn idm_account_credential_update_attested_passkey_init(
1897        &self,
1898        session_token: &CUSessionToken,
1899    ) -> Result<CUStatus, ClientError> {
1900        let scr = CURequest::AttestedPasskeyInit;
1901        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1902            .await
1903    }
1904
1905    pub async fn idm_account_credential_update_attested_passkey_finish(
1906        &self,
1907        session_token: &CUSessionToken,
1908        label: String,
1909        registration: RegisterPublicKeyCredential,
1910    ) -> Result<CUStatus, ClientError> {
1911        let scr = CURequest::AttestedPasskeyFinish(label, registration);
1912        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1913            .await
1914    }
1915
1916    pub async fn idm_account_credential_update_attested_passkey_remove(
1917        &self,
1918        session_token: &CUSessionToken,
1919        uuid: Uuid,
1920    ) -> Result<CUStatus, ClientError> {
1921        let scr = CURequest::AttestedPasskeyRemove(uuid);
1922        self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1923            .await
1924    }
1925
1926    pub async fn idm_account_credential_update_commit(
1927        &self,
1928        session_token: &CUSessionToken,
1929    ) -> Result<(), ClientError> {
1930        self.perform_simple_post_request("/v1/credential/_commit", &session_token)
1931            .await
1932    }
1933
1934    // == radius
1935
1936    pub async fn idm_account_radius_token_get(
1937        &self,
1938        id: &str,
1939    ) -> Result<RadiusAuthToken, ClientError> {
1940        self.perform_get_request(&format!("/v1/account/{id}/_radius/_token"))
1941            .await
1942    }
1943
1944    pub async fn idm_account_unix_cred_verify(
1945        &self,
1946        id: &str,
1947        cred: &str,
1948    ) -> Result<Option<UnixUserToken>, ClientError> {
1949        let req = SingleStringRequest {
1950            value: cred.to_string(),
1951        };
1952        self.perform_post_request(&format!("/v1/account/{id}/_unix/_auth"), req)
1953            .await
1954    }
1955
1956    // == generic ssh key handlers
1957    // These return the ssh keys in their "authorized keys" form rather than a format that
1958    // shows labels and can be easily updated.
1959    pub async fn idm_account_get_ssh_pubkey(
1960        &self,
1961        id: &str,
1962        tag: &str,
1963    ) -> Result<Option<String>, ClientError> {
1964        self.perform_get_request(&format!("/v1/account/{id}/_ssh_pubkeys/{tag}"))
1965            .await
1966    }
1967
1968    pub async fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result<Vec<String>, ClientError> {
1969        self.perform_get_request(&format!("/v1/account/{id}/_ssh_pubkeys"))
1970            .await
1971    }
1972
1973    // ==== domain_info (aka domain)
1974    pub async fn idm_domain_get(&self) -> Result<Entry, ClientError> {
1975        let r: Result<Vec<Entry>, ClientError> = self.perform_get_request("/v1/domain").await;
1976        r.and_then(|mut v| v.pop().ok_or(ClientError::EmptyResponse))
1977    }
1978
1979    /// Sets the domain display name using a PUT request
1980    pub async fn idm_domain_set_display_name(
1981        &self,
1982        new_display_name: &str,
1983    ) -> Result<(), ClientError> {
1984        self.perform_put_request(
1985            &format!("/v1/domain/_attr/{ATTR_DOMAIN_DISPLAY_NAME}"),
1986            vec![new_display_name],
1987        )
1988        .await
1989    }
1990
1991    pub async fn idm_domain_set_ldap_basedn(&self, new_basedn: &str) -> Result<(), ClientError> {
1992        self.perform_put_request(
1993            &format!("/v1/domain/_attr/{ATTR_DOMAIN_LDAP_BASEDN}"),
1994            vec![new_basedn],
1995        )
1996        .await
1997    }
1998
1999    /// Sets the maximum number of LDAP attributes that can be queryed in a single operation
2000    pub async fn idm_domain_set_ldap_max_queryable_attrs(
2001        &self,
2002        max_queryable_attrs: usize,
2003    ) -> Result<(), ClientError> {
2004        self.perform_put_request(
2005            &format!("/v1/domain/_attr/{ATTR_LDAP_MAX_QUERYABLE_ATTRS}"),
2006            vec![max_queryable_attrs.to_string()],
2007        )
2008        .await
2009    }
2010
2011    pub async fn idm_set_ldap_allow_unix_password_bind(
2012        &self,
2013        enable: bool,
2014    ) -> Result<(), ClientError> {
2015        self.perform_put_request(
2016            &format!("{}{}", "/v1/domain/_attr/", ATTR_LDAP_ALLOW_UNIX_PW_BIND),
2017            vec![enable.to_string()],
2018        )
2019        .await
2020    }
2021
2022    pub async fn idm_domain_get_ssid(&self) -> Result<String, ClientError> {
2023        self.perform_get_request(&format!("/v1/domain/_attr/{ATTR_DOMAIN_SSID}"))
2024            .await
2025            .and_then(|mut r: Vec<String>|
2026                // Get the first result
2027                r.pop()
2028                .ok_or(
2029                    ClientError::EmptyResponse
2030                ))
2031    }
2032
2033    pub async fn idm_domain_set_ssid(&self, ssid: &str) -> Result<(), ClientError> {
2034        self.perform_put_request(
2035            &format!("/v1/domain/_attr/{ATTR_DOMAIN_SSID}"),
2036            vec![ssid.to_string()],
2037        )
2038        .await
2039    }
2040
2041    pub async fn idm_domain_revoke_key(&self, key_id: &str) -> Result<(), ClientError> {
2042        self.perform_put_request(
2043            &format!("/v1/domain/_attr/{ATTR_KEY_ACTION_REVOKE}"),
2044            vec![key_id.to_string()],
2045        )
2046        .await
2047    }
2048
2049    // ==== schema
2050    pub async fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
2051        self.perform_get_request("/v1/schema").await
2052    }
2053
2054    pub async fn idm_schema_attributetype_list(&self) -> Result<Vec<Entry>, ClientError> {
2055        self.perform_get_request("/v1/schema/attributetype").await
2056    }
2057
2058    pub async fn idm_schema_attributetype_get(
2059        &self,
2060        id: &str,
2061    ) -> Result<Option<Entry>, ClientError> {
2062        self.perform_get_request(&format!("/v1/schema/attributetype/{id}"))
2063            .await
2064    }
2065
2066    pub async fn idm_schema_classtype_list(&self) -> Result<Vec<Entry>, ClientError> {
2067        self.perform_get_request("/v1/schema/classtype").await
2068    }
2069
2070    pub async fn idm_schema_classtype_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
2071        self.perform_get_request(&format!("/v1/schema/classtype/{id}"))
2072            .await
2073    }
2074
2075    // ==== recycle bin
2076    pub async fn recycle_bin_list(&self) -> Result<Vec<Entry>, ClientError> {
2077        self.perform_get_request("/v1/recycle_bin").await
2078    }
2079
2080    pub async fn recycle_bin_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
2081        self.perform_get_request(&format!("/v1/recycle_bin/{id}"))
2082            .await
2083    }
2084
2085    pub async fn recycle_bin_revive(&self, id: &str) -> Result<(), ClientError> {
2086        self.perform_post_request(&format!("/v1/recycle_bin/{id}/_revive"), ())
2087            .await
2088    }
2089}
2090
2091#[cfg(test)]
2092mod tests {
2093    use super::{KanidmClient, KanidmClientBuilder};
2094    use kanidm_proto::constants::CLIENT_TOKEN_CACHE;
2095    use reqwest::StatusCode;
2096    use url::Url;
2097
2098    #[tokio::test]
2099    async fn test_no_client_version_check_on_502() {
2100        let res = reqwest::Response::from(
2101            http::Response::builder()
2102                .status(StatusCode::GATEWAY_TIMEOUT)
2103                .body("")
2104                .unwrap(),
2105        );
2106        let client = KanidmClientBuilder::new()
2107            .address("http://localhost:8080".to_string())
2108            .enable_native_ca_roots(false)
2109            .build()
2110            .expect("Failed to build client");
2111        eprintln!("This should pass because we are returning 504 and shouldn't check version...");
2112        client.expect_version(&res).await;
2113
2114        let res = reqwest::Response::from(
2115            http::Response::builder()
2116                .status(StatusCode::BAD_GATEWAY)
2117                .body("")
2118                .unwrap(),
2119        );
2120        let client = KanidmClientBuilder::new()
2121            .address("http://localhost:8080".to_string())
2122            .enable_native_ca_roots(false)
2123            .build()
2124            .expect("Failed to build client");
2125        eprintln!("This should pass because we are returning 502 and shouldn't check version...");
2126        client.expect_version(&res).await;
2127    }
2128
2129    #[test]
2130    fn test_make_url() {
2131        use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS;
2132        let client: KanidmClient = KanidmClientBuilder::new()
2133            .address(format!("https://{DEFAULT_SERVER_ADDRESS}"))
2134            .enable_native_ca_roots(false)
2135            .build()
2136            .unwrap();
2137        assert_eq!(
2138            client.get_url(),
2139            Url::parse(&format!("https://{DEFAULT_SERVER_ADDRESS}")).unwrap()
2140        );
2141        assert_eq!(
2142            client.make_url("/hello"),
2143            Url::parse(&format!("https://{DEFAULT_SERVER_ADDRESS}/hello")).unwrap()
2144        );
2145
2146        let client: KanidmClient = KanidmClientBuilder::new()
2147            .address(format!("https://{DEFAULT_SERVER_ADDRESS}/cheese/"))
2148            .enable_native_ca_roots(false)
2149            .build()
2150            .unwrap();
2151        assert_eq!(
2152            client.make_url("hello"),
2153            Url::parse(&format!("https://{DEFAULT_SERVER_ADDRESS}/cheese/hello")).unwrap()
2154        );
2155    }
2156
2157    #[test]
2158    fn test_kanidmclientbuilder_display() {
2159        let defaultclient = KanidmClientBuilder::default();
2160        println!("{defaultclient}");
2161        assert!(defaultclient.to_string().contains("verify_ca"));
2162
2163        let testclient = KanidmClientBuilder {
2164            address: Some("https://example.com".to_string()),
2165            verify_ca: true,
2166            verify_hostnames: true,
2167            ca: None,
2168            connect_timeout: Some(420),
2169            request_timeout: Some(69),
2170            use_system_proxies: true,
2171            token_cache_path: Some(CLIENT_TOKEN_CACHE.to_string()),
2172            disable_system_ca_store: false,
2173        };
2174        println!("testclient {testclient}");
2175        assert!(testclient.to_string().contains("verify_ca: true"));
2176        assert!(testclient.to_string().contains("verify_hostnames: true"));
2177
2178        let badness = testclient.danger_accept_invalid_hostnames(true);
2179        let badness = badness.danger_accept_invalid_certs(true);
2180        println!("badness: {badness}");
2181        assert!(badness.to_string().contains("verify_ca: false"));
2182        assert!(badness.to_string().contains("verify_hostnames: false"));
2183    }
2184}