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 mut client_builder = reqwest::Client::builder()
519 .user_agent(KanidmClientBuilder::user_agent())
520 .cookie_store(true)
523 .cookie_provider(client_cookies.clone());
524
525 if let Some(cacert) = self.ca.as_ref() {
526 if self.disable_system_ca_store {
527 client_builder = client_builder.tls_certs_only(vec![cacert.clone()])
528 }
529 };
530
531 client_builder = client_builder
532 .danger_accept_invalid_hostnames(!self.verify_hostnames)
534 .danger_accept_invalid_certs(!self.verify_ca);
535
536 let client_builder = match self.use_system_proxies {
537 true => client_builder,
538 false => client_builder.no_proxy(),
539 };
540
541 let client_builder = match &self.ca {
542 Some(cert) => client_builder.add_root_certificate(cert.clone()),
543 None => client_builder,
544 };
545
546 let client_builder = match &self.connect_timeout {
547 Some(secs) => client_builder.connect_timeout(Duration::from_secs(*secs)),
548 None => client_builder,
549 };
550
551 let client_builder = match &self.request_timeout {
552 Some(secs) => client_builder.timeout(Duration::from_secs(*secs)),
553 None => client_builder,
554 };
555
556 let client = client_builder.build().map_err(ClientError::Transport)?;
557
558 #[allow(clippy::expect_used)]
560 let uri = Url::parse(&address).expect("failed to parse address");
561
562 #[allow(clippy::expect_used)]
563 let origin =
564 Url::parse(&uri.origin().ascii_serialization()).expect("failed to parse origin");
565
566 let token_cache_path = match self.token_cache_path.clone() {
567 Some(val) => val.to_string(),
568 None => CLIENT_TOKEN_CACHE.to_string(),
569 };
570
571 Ok(KanidmClient {
572 client,
573 client_cookies,
574 addr: address,
575 builder: self,
576 bearer_token: RwLock::new(None),
577 auth_session_id: RwLock::new(None),
578 origin,
579 check_version: Mutex::new(true),
580 token_cache_path,
581 })
582 }
583}
584
585fn find_reqwest_error_source<E: std::error::Error + 'static>(
588 orig: &dyn std::error::Error,
589) -> Option<&E> {
590 let mut cause = orig.source();
591 while let Some(err) = cause {
592 if let Some(typed) = err.downcast_ref::<E>() {
593 return Some(typed);
594 }
595 cause = err.source();
596 }
597
598 None
600}
601
602impl KanidmClient {
603 pub fn client(&self) -> &reqwest::Client {
605 &self.client
606 }
607
608 pub fn get_origin(&self) -> &Url {
609 &self.origin
610 }
611
612 pub fn get_url(&self) -> Url {
614 #[allow(clippy::panic)]
615 match self.addr.parse::<Url>() {
616 Ok(val) => val,
617 Err(err) => panic!("Failed to parse {} into URL: {:?}", self.addr, err),
618 }
619 }
620
621 pub fn make_url(&self, endpoint: &str) -> Url {
623 #[allow(clippy::expect_used)]
624 self.get_url().join(endpoint).expect("Failed to join URL")
625 }
626
627 pub async fn set_token(&self, new_token: String) {
628 let mut tguard = self.bearer_token.write().await;
629 *tguard = Some(new_token);
630 }
631
632 pub async fn get_token(&self) -> Option<String> {
633 let tguard = self.bearer_token.read().await;
634 (*tguard).as_ref().cloned()
635 }
636
637 pub fn new_session(&self) -> Result<Self, ClientError> {
638 let builder = self.builder.clone();
640 builder.build()
641 }
642
643 pub async fn logout(&self) -> Result<(), ClientError> {
644 match self.perform_get_request("/v1/logout").await {
645 Err(ClientError::Unauthorized)
646 | Err(ClientError::Http(reqwest::StatusCode::UNAUTHORIZED, _, _))
647 | Ok(()) => {
648 let mut tguard = self.bearer_token.write().await;
649 *tguard = None;
650 Ok(())
651 }
652 e => e,
653 }
654 }
655
656 pub fn get_token_cache_path(&self) -> String {
657 self.token_cache_path.clone()
658 }
659
660 async fn expect_version(&self, response: &reqwest::Response) {
662 let mut guard = self.check_version.lock().await;
663
664 if !*guard {
665 return;
666 }
667
668 if response.status() == StatusCode::BAD_GATEWAY
669 || response.status() == StatusCode::GATEWAY_TIMEOUT
670 {
671 debug!("Gateway error in response - we're going through a proxy so the version check is skipped.");
673 *guard = false;
674 return;
675 }
676
677 let ver: &str = response
678 .headers()
679 .get(KVERSION)
680 .and_then(|hv| hv.to_str().ok())
681 .unwrap_or("");
682
683 let matching = ver == EXPECT_VERSION;
684
685 if !matching {
686 warn!(server_version = ?ver, client_version = ?EXPECT_VERSION, "Mismatched client and server version - features may not work, or other unforeseen errors may occur.")
687 }
688
689 #[cfg(any(test, debug_assertions))]
690 if !matching && std::env::var("KANIDM_DEV_YOLO").is_err() {
691 eprintln!("⚠️ You're in debug/dev mode, so we're going to quit here.");
692 eprintln!("If you really must do this, set KANIDM_DEV_YOLO=1");
693 std::process::exit(1);
694 }
695
696 *guard = false;
698 }
699
700 pub fn handle_response_error(&self, error: reqwest::Error) -> ClientError {
702 if error.is_connect() {
703 if find_reqwest_error_source::<std::io::Error>(&error).is_some() {
704 trace!("Got an IO error! {:?}", &error);
706 return ClientError::Transport(error);
707 }
708 if let Some(hyper_error) = find_reqwest_error_source::<hyper::Error>(&error) {
709 if format!("{hyper_error:?}")
712 .to_lowercase()
713 .contains("certificate")
714 {
715 return ClientError::UntrustedCertificate(format!("{hyper_error}"));
716 }
717 }
718 }
719 ClientError::Transport(error)
720 }
721
722 fn get_kopid_from_response(&self, response: &Response) -> String {
723 let opid = response
724 .headers()
725 .get(KOPID)
726 .and_then(|hv| hv.to_str().ok())
727 .unwrap_or("missing_kopid")
728 .to_string();
729
730 debug!("opid -> {:?}", opid);
731 opid
732 }
733
734 async fn perform_simple_post_request<R: Serialize, T: DeserializeOwned>(
735 &self,
736 dest: &str,
737 request: &R,
738 ) -> Result<T, ClientError> {
739 let response = self.client.post(self.make_url(dest)).json(request);
740
741 let response = response
742 .send()
743 .await
744 .map_err(|err| self.handle_response_error(err))?;
745
746 self.expect_version(&response).await;
747
748 let opid = self.get_kopid_from_response(&response);
749
750 self.ok_or_clienterror(&opid, response)
751 .await?
752 .json()
753 .await
754 .map_err(|e| ClientError::JsonDecode(e, opid))
755 }
756
757 async fn perform_auth_post_request<R: Serialize, T: DeserializeOwned>(
758 &self,
759 dest: &str,
760 request: R,
761 ) -> Result<T, ClientError> {
762 trace!("perform_auth_post_request connecting to {}", dest);
763
764 let auth_url = self.make_url(dest);
765
766 let response = self.client.post(auth_url.clone()).json(&request);
767
768 let response = {
770 let tguard = self.bearer_token.read().await;
771 if let Some(token) = &(*tguard) {
772 response.bearer_auth(token)
773 } else {
774 response
775 }
776 };
777
778 let response = {
781 let sguard = self.auth_session_id.read().await;
782 if let Some(sessionid) = &(*sguard) {
783 response.header(KSESSIONID, sessionid)
784 } else {
785 response
786 }
787 };
788
789 let response = response
790 .send()
791 .await
792 .map_err(|err| self.handle_response_error(err))?;
793
794 self.expect_version(&response).await;
795
796 let opid = self.get_kopid_from_response(&response);
798
799 let response = self.ok_or_clienterror(&opid, response).await?;
800
801 let cookie_present = self
804 .client_cookies
805 .cookies(&auth_url)
806 .map(|cookie_header| {
807 cookie_header
808 .to_str()
809 .ok()
810 .map(|cookie_str| {
811 cookie_str
812 .split(';')
813 .filter_map(|c| c.split_once('='))
814 .any(|(name, _)| name == COOKIE_AUTH_SESSION_ID)
815 })
816 .unwrap_or_default()
817 })
818 .unwrap_or_default();
819
820 {
821 let headers = response.headers();
822
823 let mut sguard = self.auth_session_id.write().await;
824 trace!(?cookie_present);
825 if cookie_present {
826 *sguard = None;
828 } else {
829 debug!("Auth SessionID cookie not present, falling back to header.");
831 *sguard = headers
832 .get(KSESSIONID)
833 .and_then(|hv| hv.to_str().ok().map(str::to_string));
834 }
835 }
836
837 response
838 .json()
839 .await
840 .map_err(|e| ClientError::JsonDecode(e, opid))
841 }
842
843 pub async fn perform_post_request<R: Serialize, T: DeserializeOwned>(
844 &self,
845 dest: &str,
846 request: R,
847 ) -> Result<T, ClientError> {
848 let response = self.client.post(self.make_url(dest)).json(&request);
849
850 let response = {
851 let tguard = self.bearer_token.read().await;
852 if let Some(token) = &(*tguard) {
853 response.bearer_auth(token)
854 } else {
855 response
856 }
857 };
858
859 let response = response
860 .send()
861 .await
862 .map_err(|err| self.handle_response_error(err))?;
863
864 self.expect_version(&response).await;
865
866 let opid = self.get_kopid_from_response(&response);
867
868 self.ok_or_clienterror(&opid, response)
869 .await?
870 .json()
871 .await
872 .map_err(|e| ClientError::JsonDecode(e, opid))
873 }
874
875 async fn perform_put_request<R: Serialize, T: DeserializeOwned>(
876 &self,
877 dest: &str,
878 request: R,
879 ) -> Result<T, ClientError> {
880 let response = self.client.put(self.make_url(dest)).json(&request);
881
882 let response = {
883 let tguard = self.bearer_token.read().await;
884 if let Some(token) = &(*tguard) {
885 response.bearer_auth(token)
886 } else {
887 response
888 }
889 };
890
891 let response = response
892 .send()
893 .await
894 .map_err(|err| self.handle_response_error(err))?;
895
896 self.expect_version(&response).await;
897
898 let opid = self.get_kopid_from_response(&response);
899
900 match response.status() {
901 reqwest::StatusCode::OK => {}
902 reqwest::StatusCode::UNPROCESSABLE_ENTITY => {
903 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() )))
904 }
905
906 unexpected => {
907 return Err(ClientError::Http(
908 unexpected,
909 response.json().await.ok(),
910 opid,
911 ))
912 }
913 }
914
915 response
916 .json()
917 .await
918 .map_err(|e| ClientError::JsonDecode(e, opid))
919 }
920
921 async fn ok_or_clienterror(
923 &self,
924 opid: &str,
925 response: reqwest::Response,
926 ) -> Result<reqwest::Response, ClientError> {
927 match response.status() {
928 reqwest::StatusCode::OK => Ok(response),
929 unexpected => Err(ClientError::Http(
930 unexpected,
931 response.json().await.ok(),
932 opid.to_string(),
933 )),
934 }
935 }
936
937 pub async fn perform_patch_request<R: Serialize, T: DeserializeOwned>(
938 &self,
939 dest: &str,
940 request: R,
941 ) -> Result<T, ClientError> {
942 let response = self.client.patch(self.make_url(dest)).json(&request);
943
944 let response = {
945 let tguard = self.bearer_token.read().await;
946 if let Some(token) = &(*tguard) {
947 response.bearer_auth(token)
948 } else {
949 response
950 }
951 };
952
953 let response = response
954 .send()
955 .await
956 .map_err(|err| self.handle_response_error(err))?;
957
958 self.expect_version(&response).await;
959
960 let opid = self.get_kopid_from_response(&response);
961
962 self.ok_or_clienterror(&opid, response)
963 .await?
964 .json()
965 .await
966 .map_err(|e| ClientError::JsonDecode(e, opid))
967 }
968
969 #[instrument(level = "debug", skip(self))]
970 pub async fn perform_get_request<T: DeserializeOwned>(
971 &self,
972 dest: &str,
973 ) -> Result<T, ClientError> {
974 let query: Option<()> = None;
975 self.perform_get_request_query(dest, query).await
976 }
977
978 #[instrument(level = "debug", skip(self))]
979 pub async fn perform_get_request_query<T: DeserializeOwned, Q: Serialize + Debug>(
980 &self,
981 dest: &str,
982 query: Option<Q>,
983 ) -> Result<T, ClientError> {
984 let mut dest_url = self.make_url(dest);
985
986 if let Some(query) = query {
987 let txt = serde_urlencoded::to_string(&query).map_err(ClientError::UrlEncode)?;
988
989 if !txt.is_empty() {
990 dest_url.set_query(Some(txt.as_str()));
991 }
992 }
993
994 let response = self.client.get(dest_url);
995 let response = {
996 let tguard = self.bearer_token.read().await;
997 if let Some(token) = &(*tguard) {
998 response.bearer_auth(token)
999 } else {
1000 response
1001 }
1002 };
1003
1004 let response = response
1005 .send()
1006 .await
1007 .map_err(|err| self.handle_response_error(err))?;
1008
1009 self.expect_version(&response).await;
1010
1011 let opid = self.get_kopid_from_response(&response);
1012
1013 self.ok_or_clienterror(&opid, response)
1014 .await?
1015 .json()
1016 .await
1017 .map_err(|e| ClientError::JsonDecode(e, opid))
1018 }
1019
1020 async fn perform_delete_request(&self, dest: &str) -> Result<(), ClientError> {
1021 let response = self
1022 .client
1023 .delete(self.make_url(dest))
1024 .json(&serde_json::json!([]));
1026
1027 let response = {
1028 let tguard = self.bearer_token.read().await;
1029 if let Some(token) = &(*tguard) {
1030 response.bearer_auth(token)
1031 } else {
1032 response
1033 }
1034 };
1035
1036 let response = response
1037 .send()
1038 .await
1039 .map_err(|err| self.handle_response_error(err))?;
1040
1041 self.expect_version(&response).await;
1042
1043 let opid = self.get_kopid_from_response(&response);
1044
1045 self.ok_or_clienterror(&opid, response)
1046 .await?
1047 .json()
1048 .await
1049 .map_err(|e| ClientError::JsonDecode(e, opid))
1050 }
1051
1052 async fn perform_delete_request_with_body<R: Serialize>(
1053 &self,
1054 dest: &str,
1055 request: R,
1056 ) -> Result<(), ClientError> {
1057 let response = self.client.delete(self.make_url(dest)).json(&request);
1058
1059 let response = {
1060 let tguard = self.bearer_token.read().await;
1061 if let Some(token) = &(*tguard) {
1062 response.bearer_auth(token)
1063 } else {
1064 response
1065 }
1066 };
1067
1068 let response = response
1069 .send()
1070 .await
1071 .map_err(|err| self.handle_response_error(err))?;
1072
1073 self.expect_version(&response).await;
1074
1075 let opid = self.get_kopid_from_response(&response);
1076
1077 self.ok_or_clienterror(&opid, response)
1078 .await?
1079 .json()
1080 .await
1081 .map_err(|e| ClientError::JsonDecode(e, opid))
1082 }
1083
1084 #[instrument(level = "debug", skip(self))]
1085 pub async fn auth_step_init(&self, ident: &str) -> Result<Set<AuthMech>, ClientError> {
1086 let auth_init = AuthRequest {
1087 step: AuthStep::Init2 {
1088 username: ident.to_string(),
1089 issue: AuthIssueSession::Token,
1090 privileged: false,
1091 },
1092 };
1093
1094 let r: Result<AuthResponse, _> =
1095 self.perform_auth_post_request("/v1/auth", auth_init).await;
1096 r.map(|v| {
1097 debug!("Authentication Session ID -> {:?}", v.sessionid);
1098 v.state
1100 })
1101 .and_then(|state| match state {
1102 AuthState::Choose(mechs) => Ok(mechs),
1103 _ => Err(ClientError::AuthenticationFailed),
1104 })
1105 .map(|mechs| mechs.into_iter().collect())
1106 }
1107
1108 #[instrument(level = "debug", skip(self))]
1109 pub async fn auth_step_begin(&self, mech: AuthMech) -> Result<Vec<AuthAllowed>, ClientError> {
1110 let auth_begin = AuthRequest {
1111 step: AuthStep::Begin(mech),
1112 };
1113
1114 let r: Result<AuthResponse, _> =
1115 self.perform_auth_post_request("/v1/auth", auth_begin).await;
1116 r.map(|v| {
1117 debug!("Authentication Session ID -> {:?}", v.sessionid);
1118 v.state
1119 })
1120 .and_then(|state| match state {
1121 AuthState::Continue(allowed) => Ok(allowed),
1122 _ => Err(ClientError::AuthenticationFailed),
1123 })
1124 }
1127
1128 #[instrument(level = "debug", skip_all)]
1129 pub async fn auth_step_anonymous(&self) -> Result<AuthResponse, ClientError> {
1130 let auth_anon = AuthRequest {
1131 step: AuthStep::Cred(AuthCredential::Anonymous),
1132 };
1133 let r: Result<AuthResponse, _> =
1134 self.perform_auth_post_request("/v1/auth", auth_anon).await;
1135
1136 if let Ok(ar) = &r {
1137 if let AuthState::Success(token) = &ar.state {
1138 self.set_token(token.clone()).await;
1139 };
1140 };
1141 r
1142 }
1143
1144 #[instrument(level = "debug", skip_all)]
1145 pub async fn auth_step_password(&self, password: &str) -> Result<AuthResponse, ClientError> {
1146 let auth_req = AuthRequest {
1147 step: AuthStep::Cred(AuthCredential::Password(password.to_string())),
1148 };
1149 let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1150
1151 if let Ok(ar) = &r {
1152 if let AuthState::Success(token) = &ar.state {
1153 self.set_token(token.clone()).await;
1154 };
1155 };
1156 r
1157 }
1158
1159 #[instrument(level = "debug", skip_all)]
1160 pub async fn auth_step_backup_code(
1161 &self,
1162 backup_code: &str,
1163 ) -> Result<AuthResponse, ClientError> {
1164 let auth_req = AuthRequest {
1165 step: AuthStep::Cred(AuthCredential::BackupCode(backup_code.to_string())),
1166 };
1167 let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1168
1169 if let Ok(ar) = &r {
1170 if let AuthState::Success(token) = &ar.state {
1171 self.set_token(token.clone()).await;
1172 };
1173 };
1174 r
1175 }
1176
1177 #[instrument(level = "debug", skip_all)]
1178 pub async fn auth_step_totp(&self, totp: u32) -> Result<AuthResponse, ClientError> {
1179 let auth_req = AuthRequest {
1180 step: AuthStep::Cred(AuthCredential::Totp(totp)),
1181 };
1182 let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1183
1184 if let Ok(ar) = &r {
1185 if let AuthState::Success(token) = &ar.state {
1186 self.set_token(token.clone()).await;
1187 };
1188 };
1189 r
1190 }
1191
1192 #[instrument(level = "debug", skip_all)]
1193 pub async fn auth_step_securitykey_complete(
1194 &self,
1195 pkc: Box<PublicKeyCredential>,
1196 ) -> Result<AuthResponse, ClientError> {
1197 let auth_req = AuthRequest {
1198 step: AuthStep::Cred(AuthCredential::SecurityKey(pkc)),
1199 };
1200 let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1201
1202 if let Ok(ar) = &r {
1203 if let AuthState::Success(token) = &ar.state {
1204 self.set_token(token.clone()).await;
1205 };
1206 };
1207 r
1208 }
1209
1210 #[instrument(level = "debug", skip_all)]
1211 pub async fn auth_step_passkey_complete(
1212 &self,
1213 pkc: Box<PublicKeyCredential>,
1214 ) -> Result<AuthResponse, ClientError> {
1215 let auth_req = AuthRequest {
1216 step: AuthStep::Cred(AuthCredential::Passkey(pkc)),
1217 };
1218 let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
1219
1220 if let Ok(ar) = &r {
1221 if let AuthState::Success(token) = &ar.state {
1222 self.set_token(token.clone()).await;
1223 };
1224 };
1225 r
1226 }
1227
1228 #[instrument(level = "debug", skip(self))]
1229 pub async fn auth_anonymous(&self) -> Result<(), ClientError> {
1230 let mechs = match self.auth_step_init("anonymous").await {
1231 Ok(s) => s,
1232 Err(e) => return Err(e),
1233 };
1234
1235 if !mechs.contains(&AuthMech::Anonymous) {
1236 debug!("Anonymous mech not presented");
1237 return Err(ClientError::AuthenticationFailed);
1238 }
1239
1240 let _state = match self.auth_step_begin(AuthMech::Anonymous).await {
1241 Ok(s) => s,
1242 Err(e) => return Err(e),
1243 };
1244
1245 let r = self.auth_step_anonymous().await?;
1246
1247 match r.state {
1248 AuthState::Success(token) => {
1249 self.set_token(token.clone()).await;
1250 Ok(())
1251 }
1252 _ => Err(ClientError::AuthenticationFailed),
1253 }
1254 }
1255
1256 #[instrument(level = "debug", skip(self, password))]
1257 pub async fn auth_simple_password(
1258 &self,
1259 ident: &str,
1260 password: &str,
1261 ) -> Result<(), ClientError> {
1262 trace!("Init auth step");
1263 let mechs = match self.auth_step_init(ident).await {
1264 Ok(s) => s,
1265 Err(e) => return Err(e),
1266 };
1267
1268 if !mechs.contains(&AuthMech::Password) {
1269 debug!("Password mech not presented");
1270 return Err(ClientError::AuthenticationFailed);
1271 }
1272
1273 let _state = match self.auth_step_begin(AuthMech::Password).await {
1274 Ok(s) => s,
1275 Err(e) => return Err(e),
1276 };
1277
1278 let r = self.auth_step_password(password).await?;
1279
1280 match r.state {
1281 AuthState::Success(_) => Ok(()),
1282 _ => Err(ClientError::AuthenticationFailed),
1283 }
1284 }
1285
1286 #[instrument(level = "debug", skip(self, password, totp))]
1287 pub async fn auth_password_totp(
1288 &self,
1289 ident: &str,
1290 password: &str,
1291 totp: u32,
1292 ) -> Result<(), ClientError> {
1293 let mechs = match self.auth_step_init(ident).await {
1294 Ok(s) => s,
1295 Err(e) => return Err(e),
1296 };
1297
1298 if !mechs.contains(&AuthMech::PasswordTotp) {
1299 debug!("PasswordTotp mech not presented");
1300 return Err(ClientError::AuthenticationFailed);
1301 }
1302
1303 let state = match self.auth_step_begin(AuthMech::PasswordTotp).await {
1304 Ok(s) => s,
1305 Err(e) => return Err(e),
1306 };
1307
1308 if !state.contains(&AuthAllowed::Totp) {
1309 debug!("TOTP step not offered.");
1310 return Err(ClientError::AuthenticationFailed);
1311 }
1312
1313 let r = self.auth_step_totp(totp).await?;
1314
1315 match r.state {
1317 AuthState::Continue(allowed) => {
1318 if !allowed.contains(&AuthAllowed::Password) {
1319 debug!("Password step not offered.");
1320 return Err(ClientError::AuthenticationFailed);
1321 }
1322 }
1323 _ => {
1324 debug!("Invalid AuthState presented.");
1325 return Err(ClientError::AuthenticationFailed);
1326 }
1327 };
1328
1329 let r = self.auth_step_password(password).await?;
1330
1331 match r.state {
1332 AuthState::Success(_token) => Ok(()),
1333 _ => Err(ClientError::AuthenticationFailed),
1334 }
1335 }
1336
1337 #[instrument(level = "debug", skip(self, password, backup_code))]
1338 pub async fn auth_password_backup_code(
1339 &self,
1340 ident: &str,
1341 password: &str,
1342 backup_code: &str,
1343 ) -> Result<(), ClientError> {
1344 let mechs = match self.auth_step_init(ident).await {
1345 Ok(s) => s,
1346 Err(e) => return Err(e),
1347 };
1348
1349 if !mechs.contains(&AuthMech::PasswordBackupCode) {
1350 debug!("PasswordBackupCode mech not presented");
1351 return Err(ClientError::AuthenticationFailed);
1352 }
1353
1354 let state = match self.auth_step_begin(AuthMech::PasswordBackupCode).await {
1355 Ok(s) => s,
1356 Err(e) => return Err(e),
1357 };
1358
1359 if !state.contains(&AuthAllowed::BackupCode) {
1360 debug!("Backup Code step not offered.");
1361 return Err(ClientError::AuthenticationFailed);
1362 }
1363
1364 let r = self.auth_step_backup_code(backup_code).await?;
1365
1366 match r.state {
1368 AuthState::Continue(allowed) => {
1369 if !allowed.contains(&AuthAllowed::Password) {
1370 debug!("Password step not offered.");
1371 return Err(ClientError::AuthenticationFailed);
1372 }
1373 }
1374 _ => {
1375 debug!("Invalid AuthState presented.");
1376 return Err(ClientError::AuthenticationFailed);
1377 }
1378 };
1379
1380 let r = self.auth_step_password(password).await?;
1381
1382 match r.state {
1383 AuthState::Success(_token) => Ok(()),
1384 _ => Err(ClientError::AuthenticationFailed),
1385 }
1386 }
1387
1388 #[instrument(level = "debug", skip(self))]
1389 pub async fn auth_passkey_begin(
1390 &self,
1391 ident: &str,
1392 ) -> Result<RequestChallengeResponse, ClientError> {
1393 let mechs = match self.auth_step_init(ident).await {
1394 Ok(s) => s,
1395 Err(e) => return Err(e),
1396 };
1397
1398 if !mechs.contains(&AuthMech::Passkey) {
1399 debug!("Webauthn mech not presented");
1400 return Err(ClientError::AuthenticationFailed);
1401 }
1402
1403 let state = match self.auth_step_begin(AuthMech::Passkey).await {
1404 Ok(mut s) => s.pop(),
1405 Err(e) => return Err(e),
1406 };
1407
1408 match state {
1410 Some(AuthAllowed::Passkey(r)) => Ok(r),
1411 _ => Err(ClientError::AuthenticationFailed),
1412 }
1413 }
1414
1415 #[instrument(level = "debug", skip_all)]
1416 pub async fn auth_passkey_complete(
1417 &self,
1418 pkc: Box<PublicKeyCredential>,
1419 ) -> Result<(), ClientError> {
1420 let r = self.auth_step_passkey_complete(pkc).await?;
1421 match r.state {
1422 AuthState::Success(_token) => Ok(()),
1423 _ => Err(ClientError::AuthenticationFailed),
1424 }
1425 }
1426
1427 pub async fn reauth_begin(&self) -> Result<Vec<AuthAllowed>, ClientError> {
1428 let issue = AuthIssueSession::Token;
1429 let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/reauth", issue).await;
1430
1431 r.map(|v| {
1432 debug!("Authentication Session ID -> {:?}", v.sessionid);
1433 v.state
1434 })
1435 .and_then(|state| match state {
1436 AuthState::Continue(allowed) => Ok(allowed),
1437 _ => Err(ClientError::AuthenticationFailed),
1438 })
1439 }
1440
1441 #[instrument(level = "debug", skip_all)]
1442 pub async fn reauth_simple_password(&self, password: &str) -> Result<(), ClientError> {
1443 let state = match self.reauth_begin().await {
1444 Ok(mut s) => s.pop(),
1445 Err(e) => return Err(e),
1446 };
1447
1448 match state {
1449 Some(AuthAllowed::Password) => {}
1450 _ => {
1451 return Err(ClientError::AuthenticationFailed);
1452 }
1453 };
1454
1455 let r = self.auth_step_password(password).await?;
1456
1457 match r.state {
1458 AuthState::Success(_) => Ok(()),
1459 _ => Err(ClientError::AuthenticationFailed),
1460 }
1461 }
1462
1463 #[instrument(level = "debug", skip_all)]
1464 pub async fn reauth_password_totp(&self, password: &str, totp: u32) -> Result<(), ClientError> {
1465 let state = match self.reauth_begin().await {
1466 Ok(s) => s,
1467 Err(e) => return Err(e),
1468 };
1469
1470 if !state.contains(&AuthAllowed::Totp) {
1471 debug!("TOTP step not offered.");
1472 return Err(ClientError::AuthenticationFailed);
1473 }
1474
1475 let r = self.auth_step_totp(totp).await?;
1476
1477 match r.state {
1479 AuthState::Continue(allowed) => {
1480 if !allowed.contains(&AuthAllowed::Password) {
1481 debug!("Password step not offered.");
1482 return Err(ClientError::AuthenticationFailed);
1483 }
1484 }
1485 _ => {
1486 debug!("Invalid AuthState presented.");
1487 return Err(ClientError::AuthenticationFailed);
1488 }
1489 };
1490
1491 let r = self.auth_step_password(password).await?;
1492
1493 match r.state {
1494 AuthState::Success(_token) => Ok(()),
1495 _ => Err(ClientError::AuthenticationFailed),
1496 }
1497 }
1498
1499 #[instrument(level = "debug", skip_all)]
1500 pub async fn reauth_passkey_begin(&self) -> Result<RequestChallengeResponse, ClientError> {
1501 let state = match self.reauth_begin().await {
1502 Ok(mut s) => s.pop(),
1503 Err(e) => return Err(e),
1504 };
1505
1506 match state {
1508 Some(AuthAllowed::Passkey(r)) => Ok(r),
1509 _ => Err(ClientError::AuthenticationFailed),
1510 }
1511 }
1512
1513 #[instrument(level = "debug", skip_all)]
1514 pub async fn reauth_passkey_complete(
1515 &self,
1516 pkc: Box<PublicKeyCredential>,
1517 ) -> Result<(), ClientError> {
1518 let r = self.auth_step_passkey_complete(pkc).await?;
1519 match r.state {
1520 AuthState::Success(_token) => Ok(()),
1521 _ => Err(ClientError::AuthenticationFailed),
1522 }
1523 }
1524
1525 pub async fn auth_valid(&self) -> Result<(), ClientError> {
1526 self.perform_get_request(V1_AUTH_VALID).await
1527 }
1528
1529 pub async fn get_public_jwk(&self, key_id: &str) -> Result<Jwk, ClientError> {
1530 self.perform_get_request(&format!("/v1/jwk/{key_id}")).await
1531 }
1532
1533 pub async fn whoami(&self) -> Result<Option<Entry>, ClientError> {
1534 let response = self.client.get(self.make_url("/v1/self"));
1535
1536 let response = {
1537 let tguard = self.bearer_token.read().await;
1538 if let Some(token) = &(*tguard) {
1539 response.bearer_auth(token)
1540 } else {
1541 response
1542 }
1543 };
1544
1545 let response = response
1546 .send()
1547 .await
1548 .map_err(|err| self.handle_response_error(err))?;
1549
1550 self.expect_version(&response).await;
1551
1552 let opid = self.get_kopid_from_response(&response);
1553 match response.status() {
1554 reqwest::StatusCode::OK => {}
1556 reqwest::StatusCode::UNAUTHORIZED => return Ok(None),
1557 unexpected => {
1558 return Err(ClientError::Http(
1559 unexpected,
1560 response.json().await.ok(),
1561 opid,
1562 ))
1563 }
1564 }
1565
1566 let r: WhoamiResponse = response
1567 .json()
1568 .await
1569 .map_err(|e| ClientError::JsonDecode(e, opid))?;
1570
1571 Ok(Some(r.youare))
1572 }
1573
1574 pub async fn search(&self, filter: Filter) -> Result<Vec<Entry>, ClientError> {
1576 let sr = SearchRequest { filter };
1577 let r: Result<SearchResponse, _> = self.perform_post_request("/v1/raw/search", sr).await;
1578 r.map(|v| v.entries)
1579 }
1580
1581 pub async fn create(&self, entries: Vec<Entry>) -> Result<(), ClientError> {
1582 let c = CreateRequest { entries };
1583 self.perform_post_request("/v1/raw/create", c).await
1584 }
1585
1586 pub async fn modify(&self, filter: Filter, modlist: ModifyList) -> Result<(), ClientError> {
1587 let mr = ModifyRequest { filter, modlist };
1588 self.perform_post_request("/v1/raw/modify", mr).await
1589 }
1590
1591 pub async fn delete(&self, filter: Filter) -> Result<(), ClientError> {
1592 let dr = DeleteRequest { filter };
1593 self.perform_post_request("/v1/raw/delete", dr).await
1594 }
1595
1596 pub async fn idm_group_list(&self) -> Result<Vec<Entry>, ClientError> {
1600 self.perform_get_request("/v1/group").await
1601 }
1602
1603 pub async fn idm_group_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
1604 self.perform_get_request(&format!("/v1/group/{id}")).await
1605 }
1606
1607 pub async fn idm_group_get_members(
1608 &self,
1609 id: &str,
1610 ) -> Result<Option<Vec<String>>, ClientError> {
1611 self.perform_get_request(&format!("/v1/group/{id}/_attr/member"))
1612 .await
1613 }
1614
1615 pub async fn idm_group_create(
1616 &self,
1617 name: &str,
1618 entry_managed_by: Option<&str>,
1619 ) -> Result<(), ClientError> {
1620 let mut new_group = Entry {
1621 attrs: BTreeMap::new(),
1622 };
1623 new_group
1624 .attrs
1625 .insert(ATTR_NAME.to_string(), vec![name.to_string()]);
1626
1627 if let Some(entry_manager) = entry_managed_by {
1628 new_group.attrs.insert(
1629 ATTR_ENTRY_MANAGED_BY.to_string(),
1630 vec![entry_manager.to_string()],
1631 );
1632 }
1633
1634 self.perform_post_request("/v1/group", new_group).await
1635 }
1636
1637 pub async fn idm_group_set_entry_managed_by(
1638 &self,
1639 id: &str,
1640 entry_manager: &str,
1641 ) -> Result<(), ClientError> {
1642 let data = vec![entry_manager];
1643 self.perform_put_request(&format!("/v1/group/{id}/_attr/entry_managed_by"), data)
1644 .await
1645 }
1646
1647 pub async fn idm_group_set_members(
1648 &self,
1649 id: &str,
1650 members: &[&str],
1651 ) -> Result<(), ClientError> {
1652 let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect();
1653 self.perform_put_request(&format!("/v1/group/{id}/_attr/member"), m)
1654 .await
1655 }
1656
1657 pub async fn idm_group_add_members(
1658 &self,
1659 id: &str,
1660 members: &[&str],
1661 ) -> Result<(), ClientError> {
1662 let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect();
1663 self.perform_post_request(&format!("/v1/group/{id}/_attr/member"), m)
1664 .await
1665 }
1666
1667 pub async fn idm_group_remove_members(
1668 &self,
1669 group: &str,
1670 members: &[&str],
1671 ) -> Result<(), ClientError> {
1672 debug!(
1673 "Asked to remove members {} from {}",
1674 &members.join(","),
1675 group
1676 );
1677 self.perform_delete_request_with_body(&format!("/v1/group/{group}/_attr/member"), &members)
1678 .await
1679 }
1680
1681 pub async fn idm_group_purge_members(&self, id: &str) -> Result<(), ClientError> {
1682 self.perform_delete_request(&format!("/v1/group/{id}/_attr/member"))
1683 .await
1684 }
1685
1686 pub async fn idm_group_unix_extend(
1687 &self,
1688 id: &str,
1689 gidnumber: Option<u32>,
1690 ) -> Result<(), ClientError> {
1691 let gx = GroupUnixExtend { gidnumber };
1692 self.perform_post_request(&format!("/v1/group/{id}/_unix"), gx)
1693 .await
1694 }
1695
1696 pub async fn idm_group_unix_token_get(&self, id: &str) -> Result<UnixGroupToken, ClientError> {
1697 self.perform_get_request(&format!("/v1/group/{id}/_unix/_token"))
1698 .await
1699 }
1700
1701 pub async fn idm_group_delete(&self, id: &str) -> Result<(), ClientError> {
1702 self.perform_delete_request(&format!("/v1/group/{id}"))
1703 .await
1704 }
1705
1706 pub async fn idm_account_unix_token_get(&self, id: &str) -> Result<UnixUserToken, ClientError> {
1709 self.perform_get_request(&format!("/v1/account/{id}/_unix/_token"))
1710 .await
1711 }
1712
1713 #[instrument(level = "debug", skip(self))]
1714 pub async fn idm_person_account_credential_update_send_intent(
1715 &self,
1716 id: &str,
1717 ttl: Option<u64>,
1718 email: Option<String>,
1719 ) -> Result<(), ClientError> {
1720 let req = CUIntentSend { ttl, email };
1721 self.perform_post_request(
1722 &format!("/v1/person/{id}/_credential/_update_intent_send"),
1723 req,
1724 )
1725 .await
1726 }
1727
1728 #[instrument(level = "debug", skip(self))]
1730 pub async fn idm_person_account_credential_update_intent(
1731 &self,
1732 id: &str,
1733 ttl: Option<u32>,
1734 ) -> Result<CUIntentToken, ClientError> {
1735 if let Some(ttl) = ttl {
1736 self.perform_get_request(&format!("/v1/person/{id}/_credential/_update_intent/{ttl}"))
1737 .await
1738 } else {
1739 self.perform_get_request(&format!("/v1/person/{id}/_credential/_update_intent"))
1740 .await
1741 }
1742 }
1743
1744 pub async fn idm_account_credential_update_begin(
1745 &self,
1746 id: &str,
1747 ) -> Result<(CUSessionToken, CUStatus), ClientError> {
1748 self.perform_get_request(&format!("/v1/person/{id}/_credential/_update"))
1749 .await
1750 }
1751
1752 pub async fn idm_account_credential_update_exchange(
1753 &self,
1754 intent_token: String,
1755 ) -> Result<(CUSessionToken, CUStatus), ClientError> {
1756 self.perform_simple_post_request("/v1/credential/_exchange_intent", &intent_token)
1758 .await
1759 }
1760
1761 pub async fn idm_account_credential_update_status(
1762 &self,
1763 session_token: &CUSessionToken,
1764 ) -> Result<CUStatus, ClientError> {
1765 self.perform_simple_post_request("/v1/credential/_status", &session_token)
1766 .await
1767 }
1768
1769 pub async fn idm_account_credential_update_set_password(
1770 &self,
1771 session_token: &CUSessionToken,
1772 pw: &str,
1773 ) -> Result<CUStatus, ClientError> {
1774 let scr = CURequest::Password(pw.to_string());
1775 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1776 .await
1777 }
1778
1779 pub async fn idm_account_credential_update_cancel_mfareg(
1780 &self,
1781 session_token: &CUSessionToken,
1782 ) -> Result<CUStatus, ClientError> {
1783 let scr = CURequest::CancelMFAReg;
1784 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1785 .await
1786 }
1787
1788 pub async fn idm_account_credential_update_init_totp(
1789 &self,
1790 session_token: &CUSessionToken,
1791 ) -> Result<CUStatus, ClientError> {
1792 let scr = CURequest::TotpGenerate;
1793 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1794 .await
1795 }
1796
1797 pub async fn idm_account_credential_update_check_totp(
1798 &self,
1799 session_token: &CUSessionToken,
1800 totp_chal: u32,
1801 label: &str,
1802 ) -> Result<CUStatus, ClientError> {
1803 let scr = CURequest::TotpVerify(totp_chal, label.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_accept_sha1_totp(
1810 &self,
1811 session_token: &CUSessionToken,
1812 ) -> Result<CUStatus, ClientError> {
1813 let scr = CURequest::TotpAcceptSha1;
1814 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1815 .await
1816 }
1817
1818 pub async fn idm_account_credential_update_remove_totp(
1819 &self,
1820 session_token: &CUSessionToken,
1821 label: &str,
1822 ) -> Result<CUStatus, ClientError> {
1823 let scr = CURequest::TotpRemove(label.to_string());
1824 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1825 .await
1826 }
1827
1828 pub async fn idm_account_credential_update_backup_codes_generate(
1830 &self,
1831 session_token: &CUSessionToken,
1832 ) -> Result<CUStatus, ClientError> {
1833 let scr = CURequest::BackupCodeGenerate;
1834 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1835 .await
1836 }
1837
1838 pub async fn idm_account_credential_update_primary_remove(
1840 &self,
1841 session_token: &CUSessionToken,
1842 ) -> Result<CUStatus, ClientError> {
1843 let scr = CURequest::PrimaryRemove;
1844 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1845 .await
1846 }
1847
1848 pub async fn idm_account_credential_update_set_unix_password(
1849 &self,
1850 session_token: &CUSessionToken,
1851 pw: &str,
1852 ) -> Result<CUStatus, ClientError> {
1853 let scr = CURequest::UnixPassword(pw.to_string());
1854 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1855 .await
1856 }
1857
1858 pub async fn idm_account_credential_update_unix_remove(
1859 &self,
1860 session_token: &CUSessionToken,
1861 ) -> Result<CUStatus, ClientError> {
1862 let scr = CURequest::UnixPasswordRemove;
1863 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1864 .await
1865 }
1866
1867 pub async fn idm_account_credential_update_sshkey_add(
1868 &self,
1869 session_token: &CUSessionToken,
1870 label: String,
1871 key: SshPublicKey,
1872 ) -> Result<CUStatus, ClientError> {
1873 let scr = CURequest::SshPublicKey(label, key);
1874 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1875 .await
1876 }
1877
1878 pub async fn idm_account_credential_update_sshkey_remove(
1879 &self,
1880 session_token: &CUSessionToken,
1881 label: String,
1882 ) -> Result<CUStatus, ClientError> {
1883 let scr = CURequest::SshPublicKeyRemove(label);
1884 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1885 .await
1886 }
1887
1888 pub async fn idm_account_credential_update_passkey_init(
1889 &self,
1890 session_token: &CUSessionToken,
1891 ) -> Result<CUStatus, ClientError> {
1892 let scr = CURequest::PasskeyInit;
1893 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1894 .await
1895 }
1896
1897 pub async fn idm_account_credential_update_passkey_finish(
1898 &self,
1899 session_token: &CUSessionToken,
1900 label: String,
1901 registration: RegisterPublicKeyCredential,
1902 ) -> Result<CUStatus, ClientError> {
1903 let scr = CURequest::PasskeyFinish(label, registration);
1904 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1905 .await
1906 }
1907
1908 pub async fn idm_account_credential_update_passkey_remove(
1910 &self,
1911 session_token: &CUSessionToken,
1912 uuid: Uuid,
1913 ) -> Result<CUStatus, ClientError> {
1914 let scr = CURequest::PasskeyRemove(uuid);
1915 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1916 .await
1917 }
1918
1919 pub async fn idm_account_credential_update_attested_passkey_init(
1920 &self,
1921 session_token: &CUSessionToken,
1922 ) -> Result<CUStatus, ClientError> {
1923 let scr = CURequest::AttestedPasskeyInit;
1924 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1925 .await
1926 }
1927
1928 pub async fn idm_account_credential_update_attested_passkey_finish(
1929 &self,
1930 session_token: &CUSessionToken,
1931 label: String,
1932 registration: RegisterPublicKeyCredential,
1933 ) -> Result<CUStatus, ClientError> {
1934 let scr = CURequest::AttestedPasskeyFinish(label, registration);
1935 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1936 .await
1937 }
1938
1939 pub async fn idm_account_credential_update_attested_passkey_remove(
1940 &self,
1941 session_token: &CUSessionToken,
1942 uuid: Uuid,
1943 ) -> Result<CUStatus, ClientError> {
1944 let scr = CURequest::AttestedPasskeyRemove(uuid);
1945 self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
1946 .await
1947 }
1948
1949 pub async fn idm_account_credential_update_commit(
1950 &self,
1951 session_token: &CUSessionToken,
1952 ) -> Result<(), ClientError> {
1953 self.perform_simple_post_request("/v1/credential/_commit", &session_token)
1954 .await
1955 }
1956
1957 pub async fn idm_account_radius_token_get(
1960 &self,
1961 id: &str,
1962 ) -> Result<RadiusAuthToken, ClientError> {
1963 self.perform_get_request(&format!("/v1/account/{id}/_radius/_token"))
1964 .await
1965 }
1966
1967 pub async fn idm_account_unix_cred_verify(
1968 &self,
1969 id: &str,
1970 cred: &str,
1971 ) -> Result<Option<UnixUserToken>, ClientError> {
1972 let req = SingleStringRequest {
1973 value: cred.to_string(),
1974 };
1975 self.perform_post_request(&format!("/v1/account/{id}/_unix/_auth"), req)
1976 .await
1977 }
1978
1979 pub async fn idm_account_get_ssh_pubkey(
1983 &self,
1984 id: &str,
1985 tag: &str,
1986 ) -> Result<Option<String>, ClientError> {
1987 self.perform_get_request(&format!("/v1/account/{id}/_ssh_pubkeys/{tag}"))
1988 .await
1989 }
1990
1991 pub async fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result<Vec<String>, ClientError> {
1992 self.perform_get_request(&format!("/v1/account/{id}/_ssh_pubkeys"))
1993 .await
1994 }
1995
1996 pub async fn idm_domain_get(&self) -> Result<Entry, ClientError> {
1998 let r: Result<Vec<Entry>, ClientError> = self.perform_get_request("/v1/domain").await;
1999 r.and_then(|mut v| v.pop().ok_or(ClientError::EmptyResponse))
2000 }
2001
2002 pub async fn idm_domain_set_display_name(
2004 &self,
2005 new_display_name: &str,
2006 ) -> Result<(), ClientError> {
2007 self.perform_put_request(
2008 &format!("/v1/domain/_attr/{ATTR_DOMAIN_DISPLAY_NAME}"),
2009 vec![new_display_name],
2010 )
2011 .await
2012 }
2013
2014 pub async fn idm_domain_set_ldap_basedn(&self, new_basedn: &str) -> Result<(), ClientError> {
2015 self.perform_put_request(
2016 &format!("/v1/domain/_attr/{ATTR_DOMAIN_LDAP_BASEDN}"),
2017 vec![new_basedn],
2018 )
2019 .await
2020 }
2021
2022 pub async fn idm_domain_set_ldap_max_queryable_attrs(
2024 &self,
2025 max_queryable_attrs: usize,
2026 ) -> Result<(), ClientError> {
2027 self.perform_put_request(
2028 &format!("/v1/domain/_attr/{ATTR_LDAP_MAX_QUERYABLE_ATTRS}"),
2029 vec![max_queryable_attrs.to_string()],
2030 )
2031 .await
2032 }
2033
2034 pub async fn idm_set_ldap_allow_unix_password_bind(
2035 &self,
2036 enable: bool,
2037 ) -> Result<(), ClientError> {
2038 self.perform_put_request(
2039 &format!("{}{}", "/v1/domain/_attr/", ATTR_LDAP_ALLOW_UNIX_PW_BIND),
2040 vec![enable.to_string()],
2041 )
2042 .await
2043 }
2044
2045 pub async fn idm_domain_get_ssid(&self) -> Result<String, ClientError> {
2046 self.perform_get_request(&format!("/v1/domain/_attr/{ATTR_DOMAIN_SSID}"))
2047 .await
2048 .and_then(|mut r: Vec<String>|
2049 r.pop()
2051 .ok_or(
2052 ClientError::EmptyResponse
2053 ))
2054 }
2055
2056 pub async fn idm_domain_set_ssid(&self, ssid: &str) -> Result<(), ClientError> {
2057 self.perform_put_request(
2058 &format!("/v1/domain/_attr/{ATTR_DOMAIN_SSID}"),
2059 vec![ssid.to_string()],
2060 )
2061 .await
2062 }
2063
2064 pub async fn idm_domain_revoke_key(&self, key_id: &str) -> Result<(), ClientError> {
2065 self.perform_put_request(
2066 &format!("/v1/domain/_attr/{ATTR_KEY_ACTION_REVOKE}"),
2067 vec![key_id.to_string()],
2068 )
2069 .await
2070 }
2071
2072 pub async fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
2074 self.perform_get_request("/v1/schema").await
2075 }
2076
2077 pub async fn idm_schema_attributetype_list(&self) -> Result<Vec<Entry>, ClientError> {
2078 self.perform_get_request("/v1/schema/attributetype").await
2079 }
2080
2081 pub async fn idm_schema_attributetype_get(
2082 &self,
2083 id: &str,
2084 ) -> Result<Option<Entry>, ClientError> {
2085 self.perform_get_request(&format!("/v1/schema/attributetype/{id}"))
2086 .await
2087 }
2088
2089 pub async fn idm_schema_classtype_list(&self) -> Result<Vec<Entry>, ClientError> {
2090 self.perform_get_request("/v1/schema/classtype").await
2091 }
2092
2093 pub async fn idm_schema_classtype_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
2094 self.perform_get_request(&format!("/v1/schema/classtype/{id}"))
2095 .await
2096 }
2097
2098 pub async fn recycle_bin_list(&self) -> Result<Vec<Entry>, ClientError> {
2100 self.perform_get_request("/v1/recycle_bin").await
2101 }
2102
2103 pub async fn recycle_bin_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
2104 self.perform_get_request(&format!("/v1/recycle_bin/{id}"))
2105 .await
2106 }
2107
2108 pub async fn recycle_bin_revive(&self, id: &str) -> Result<(), ClientError> {
2109 self.perform_post_request(&format!("/v1/recycle_bin/{id}/_revive"), ())
2110 .await
2111 }
2112}
2113
2114#[cfg(test)]
2115mod tests {
2116 use super::{KanidmClient, KanidmClientBuilder};
2117 use kanidm_proto::constants::CLIENT_TOKEN_CACHE;
2118 use reqwest::StatusCode;
2119 use url::Url;
2120
2121 #[tokio::test]
2122 async fn test_no_client_version_check_on_502() {
2123 let res = reqwest::Response::from(
2124 http::Response::builder()
2125 .status(StatusCode::GATEWAY_TIMEOUT)
2126 .body("")
2127 .unwrap(),
2128 );
2129 let client = KanidmClientBuilder::new()
2130 .address("http://localhost:8080".to_string())
2131 .enable_native_ca_roots(false)
2132 .build()
2133 .expect("Failed to build client");
2134 eprintln!("This should pass because we are returning 504 and shouldn't check version...");
2135 client.expect_version(&res).await;
2136
2137 let res = reqwest::Response::from(
2138 http::Response::builder()
2139 .status(StatusCode::BAD_GATEWAY)
2140 .body("")
2141 .unwrap(),
2142 );
2143 let client = KanidmClientBuilder::new()
2144 .address("http://localhost:8080".to_string())
2145 .enable_native_ca_roots(false)
2146 .build()
2147 .expect("Failed to build client");
2148 eprintln!("This should pass because we are returning 502 and shouldn't check version...");
2149 client.expect_version(&res).await;
2150 }
2151
2152 #[test]
2153 fn test_make_url() {
2154 use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS;
2155 let client: KanidmClient = KanidmClientBuilder::new()
2156 .address(format!("https://{DEFAULT_SERVER_ADDRESS}"))
2157 .enable_native_ca_roots(false)
2158 .build()
2159 .unwrap();
2160 assert_eq!(
2161 client.get_url(),
2162 Url::parse(&format!("https://{DEFAULT_SERVER_ADDRESS}")).unwrap()
2163 );
2164 assert_eq!(
2165 client.make_url("/hello"),
2166 Url::parse(&format!("https://{DEFAULT_SERVER_ADDRESS}/hello")).unwrap()
2167 );
2168
2169 let client: KanidmClient = KanidmClientBuilder::new()
2170 .address(format!("https://{DEFAULT_SERVER_ADDRESS}/cheese/"))
2171 .enable_native_ca_roots(false)
2172 .build()
2173 .unwrap();
2174 assert_eq!(
2175 client.make_url("hello"),
2176 Url::parse(&format!("https://{DEFAULT_SERVER_ADDRESS}/cheese/hello")).unwrap()
2177 );
2178 }
2179
2180 #[test]
2181 fn test_kanidmclientbuilder_display() {
2182 let defaultclient = KanidmClientBuilder::default();
2183 println!("{defaultclient}");
2184 assert!(defaultclient.to_string().contains("verify_ca"));
2185
2186 let testclient = KanidmClientBuilder {
2187 address: Some("https://example.com".to_string()),
2188 verify_ca: true,
2189 verify_hostnames: true,
2190 ca: None,
2191 connect_timeout: Some(420),
2192 request_timeout: Some(69),
2193 use_system_proxies: true,
2194 token_cache_path: Some(CLIENT_TOKEN_CACHE.to_string()),
2195 disable_system_ca_store: false,
2196 };
2197 println!("testclient {testclient}");
2198 assert!(testclient.to_string().contains("verify_ca: true"));
2199 assert!(testclient.to_string().contains("verify_hostnames: true"));
2200
2201 let badness = testclient.danger_accept_invalid_hostnames(true);
2202 let badness = badness.danger_accept_invalid_certs(true);
2203 println!("badness: {badness}");
2204 assert!(badness.to_string().contains("verify_ca: false"));
2205 assert!(badness.to_string().contains("verify_hostnames: false"));
2206 }
2207}