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