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