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")] use std::fs::{metadata, Metadata};
21use std::io::{ErrorKind, Read};
22#[cfg(target_family = "unix")] use 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#[derive(Debug, Deserialize, Serialize)]
89pub struct KanidmClientConfigInstance {
90 pub uri: Option<String>,
94 pub verify_hostnames: Option<bool>,
98 pub verify_ca: Option<bool>,
102 pub ca_path: Option<String>,
106
107 pub connect_timeout: Option<u64>,
109}
110
111#[derive(Debug, Deserialize, Serialize)]
112pub struct KanidmClientConfig {
123 #[serde(flatten)]
125 pub default: KanidmClientConfigInstance,
126
127 #[serde(flatten)]
128 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 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 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 #[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 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 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 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 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 pub fn enable_native_ca_roots(self, enable: bool) -> Self {
404 KanidmClientBuilder {
405 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 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 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 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 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 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 pub fn build(self) -> Result<KanidmClient, ClientError> {
502 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 .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 #[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
576fn 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 None
591}
592
593impl KanidmClient {
594 pub fn client(&self) -> &reqwest::Client {
596 &self.client
597 }
598
599 pub fn get_origin(&self) -> &Url {
600 &self.origin
601 }
602
603 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 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 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 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 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 *guard = false;
689 }
690
691 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 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 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 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 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 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 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 *sguard = None;
838 } else {
839 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 .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 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 }
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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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}