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