kanidm_client/
lib.rs

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