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