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