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