1#![deny(warnings)]
2#![warn(unused_extern_crates)]
3#![deny(clippy::todo)]
4#![deny(clippy::unimplemented)]
5#![deny(clippy::unwrap_used)]
6#![deny(clippy::panic)]
7#![deny(clippy::unreachable)]
8#![deny(clippy::await_holding_lock)]
9#![deny(clippy::needless_pass_by_value)]
10#![deny(clippy::trivially_copy_pass_by_ref)]
11#![allow(clippy::expect_used)]
13#[macro_use]
14extern crate tracing;
15
16pub(crate) use crate::common::OpType;
17use std::path::PathBuf;
18
19use identify_user_no_tui::{run_identity_verification_no_tui, IdentifyUserState};
20
21use kanidm_client::{ClientError, StatusCode};
22use url::Url;
23use uuid::Uuid;
24
25include!("../opt/kanidm.rs");
26
27mod common;
28mod domain;
29mod graph;
30mod group;
31mod oauth2;
32mod person;
33mod raw;
34mod recycle;
35mod schema;
36mod serviceaccount;
37mod session;
38mod synch;
39mod system_config;
40mod webauthn;
41
42pub(crate) fn handle_client_error(response: ClientError, _output_mode: OutputMode) {
44 match response {
45 ClientError::Http(status, error, opid) => {
46 let error_msg = match &error {
47 Some(msg) => format!(" {msg:?}"),
48 None => "".to_string(),
49 };
50 error!("OperationId: {:?}", opid);
51 if status == StatusCode::INTERNAL_SERVER_ERROR {
52 error!("Internal Server Error in response: {}", error_msg);
53 std::process::exit(1);
54 } else if status == StatusCode::NOT_FOUND {
55 error!("Item not found: Check all names are correct.");
56 std::process::exit(1);
57 } else {
58 error!("HTTP Error: {}{}", status, error_msg);
59 std::process::exit(1);
60 }
61 }
62 ClientError::Transport(e) => {
63 error!("HTTP-Transport Related Error: {:?}", e);
64 std::process::exit(1);
65 }
66 ClientError::UntrustedCertificate(e) => {
67 error!("Untrusted Certificate Error: {:?}", e);
68 std::process::exit(1);
69 }
70 _ => {
71 eprintln!("{response:?}");
72 std::process::exit(1);
73 }
74 };
75}
76
77pub(crate) fn handle_group_account_policy_error(response: ClientError, _output_mode: OutputMode) {
78 use kanidm_proto::internal::OperationError::SchemaViolation;
79 use kanidm_proto::internal::SchemaError::AttributeNotValidForClass;
80
81 if let ClientError::Http(_status, Some(SchemaViolation(AttributeNotValidForClass(att))), opid) =
82 response
83 {
84 error!("OperationId: {:?}", opid);
85 error!("Cannot update account-policy attribute {att}. Is account-policy enabled on this group?");
86 } else {
87 handle_client_error(response, _output_mode);
88 }
89}
90
91impl SelfOpt {
92 pub async fn exec(&self, opt: KanidmClientParser) {
93 match self {
94 SelfOpt::Whoami => {
95 let client = opt.to_client(OpType::Read).await;
96
97 match client.whoami().await {
98 Ok(o_ent) => {
99 match o_ent {
100 Some(ent) => {
101 opt.output_mode.print_message(ent);
102 }
103 None => {
104 error!("Authentication with cached token failed, can't query information.");
105 }
107 }
108 }
109 Err(e) => handle_client_error(e, opt.output_mode),
110 }
111 }
112 SelfOpt::IdentifyUser => {
113 let client = opt.to_client(OpType::Write).await;
114 let whoami_response = match client.whoami().await {
115 Ok(o_ent) => {
116 match o_ent {
117 Some(ent) => ent,
118 None => {
119 eprintln!("Authentication with cached token failed, can't query information."); return;
121 }
122 }
123 }
124 Err(e) => {
125 opt.output_mode
126 .print_message(format!("Error querying whoami endpoint: {e:?}")); return;
128 }
129 };
130
131 let spn =
132 match whoami_response.attrs.get("spn").and_then(|v| v.first()) {
133 Some(spn) => spn,
134 None => {
135 opt.output_mode.print_message("Failed to parse your SPN from the system's whoami endpoint, exiting!"); return;
137 }
138 };
139
140 run_identity_verification_no_tui(IdentifyUserState::Start, client, spn, None).await;
141 } }
143 }
144}
145
146impl SystemOpt {
147 pub async fn exec(&self, opt: KanidmClientParser) {
148 match self {
149 SystemOpt::Api { commands } => commands.exec(opt).await,
150 SystemOpt::PwBadlist { commands } => commands.exec(opt).await,
151 SystemOpt::DeniedNames { commands } => commands.exec(opt).await,
152 SystemOpt::Oauth2 { commands } => commands.exec(opt).await,
153 SystemOpt::Domain { commands } => commands.exec(opt).await,
154 SystemOpt::Message { commands } => commands.exec(opt).await,
155 SystemOpt::Synch { commands } => commands.exec(opt).await,
156 }
157 }
158}
159
160impl KanidmClientParser {
161 pub async fn exec(self) {
162 match self.commands.clone() {
163 KanidmClientOpt::Raw { commands } => commands.exec(self).await,
164 KanidmClientOpt::Login(lopt) => lopt.exec(self).await,
165 KanidmClientOpt::Reauth => self.reauth().await,
166 KanidmClientOpt::Logout(lopt) => lopt.exec(self).await,
167 KanidmClientOpt::Session { commands } => commands.exec(self).await,
168 KanidmClientOpt::CSelf { commands } => commands.exec(self).await,
169 KanidmClientOpt::Person { commands } => commands.exec(self).await,
170 KanidmClientOpt::ServiceAccount { commands } => commands.exec(self).await,
171 KanidmClientOpt::Group { commands } => commands.exec(self).await,
172 KanidmClientOpt::Graph(gops) => gops.exec(self).await,
173 KanidmClientOpt::System { commands } => commands.exec(self).await,
174 KanidmClientOpt::Schema {
175 commands: SchemaOpt::Class { commands },
176 } => commands.exec(self).await,
177 KanidmClientOpt::Schema {
178 commands: SchemaOpt::Attribute { commands },
179 } => commands.exec(self).await,
180 KanidmClientOpt::Recycle { commands } => commands.exec(self).await,
181 KanidmClientOpt::Version => {
182 self.output_mode
183 .print_message(format!("kanidm {}", env!("KANIDM_PKG_VERSION")));
184 }
185 }
186 }
187}
188
189pub(crate) fn password_prompt(prompt: &str) -> Option<String> {
190 for _ in 0..3 {
191 let password = dialoguer::Password::new()
192 .with_prompt(prompt)
193 .interact()
194 .ok()?;
195
196 let password_confirm = dialoguer::Password::new()
197 .with_prompt("Reenter the password to confirm: ")
198 .interact()
199 .ok()?;
200
201 if password == password_confirm {
202 return Some(password);
203 } else {
204 error!("Passwords do not match");
205 }
206 }
207 None
208}
209
210pub const IDENTITY_UNAVAILABLE_ERROR_MESSAGE: &str = "Identity verification is not available.";
211pub const CODE_FAILURE_ERROR_MESSAGE: &str = "The provided verification code is invalid. Check the code with the other person, and if it remains invalid, they may be attempting to fool you!";
212pub const INVALID_STATE_ERROR_MESSAGE: &str =
213 "The user identification flow is in an invalid state 😵😵";
214
215mod identify_user_no_tui {
216 use crate::{
217 CODE_FAILURE_ERROR_MESSAGE, IDENTITY_UNAVAILABLE_ERROR_MESSAGE, INVALID_STATE_ERROR_MESSAGE,
218 };
219
220 use kanidm_client::{ClientError, KanidmClient};
221 use kanidm_proto::internal::{IdentifyUserRequest, IdentifyUserResponse};
222
223 use dialoguer::{Confirm, Input};
224 use regex::Regex;
225 use std::{
226 io::{stdout, Write},
227 sync::LazyLock,
228 time::SystemTime,
229 };
230
231 pub static VALIDATE_TOTP_RE: LazyLock<Regex> = LazyLock::new(|| {
232 #[allow(clippy::expect_used)]
233 Regex::new(r"^\d{5,6}$").expect("Failed to parse VALIDATE_TOTP_RE") });
235
236 pub(super) enum IdentifyUserState {
237 Start,
238 IdDisplayAndSubmit,
239 SubmitCode,
240 DisplayCodeFirst { self_totp: u32, step: u32 },
241 DisplayCodeSecond { self_totp: u32, step: u32 },
242 }
243
244 fn server_error(e: &ClientError) {
245 eprintln!("Server error!"); eprintln!("{e:?}");
247 println!("Exiting...");
248 }
249
250 pub(super) async fn run_identity_verification_no_tui(
251 mut state: IdentifyUserState,
252 client: KanidmClient,
253 self_id: &str,
254 mut other_id: Option<String>,
255 ) {
256 loop {
257 match state {
258 IdentifyUserState::Start => {
259 let res = match &client
260 .idm_person_identify_user(self_id, IdentifyUserRequest::Start)
261 .await
262 {
263 Ok(res) => res.clone(),
264 Err(e) => {
265 return server_error(e);
266 }
267 };
268 match res {
269 IdentifyUserResponse::IdentityVerificationUnavailable => {
270 println!("{IDENTITY_UNAVAILABLE_ERROR_MESSAGE}");
271 return;
272 }
273 IdentifyUserResponse::IdentityVerificationAvailable => {
274 state = IdentifyUserState::IdDisplayAndSubmit;
275 }
276 _ => {
277 eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
278 return;
279 }
280 }
281 }
282 IdentifyUserState::IdDisplayAndSubmit => {
283 println!("When asked for your ID, provide the following: {self_id}");
284
285 let other_user_id: String = Input::new()
287 .with_prompt("Ask for the other person's ID, and insert it here")
288 .interact_text()
289 .expect("Failed to interact with interactive session");
290 let _ = stdout().flush();
291
292 let res = match &client
293 .idm_person_identify_user(&other_user_id, IdentifyUserRequest::Start)
294 .await
295 {
296 Ok(res) => res.clone(),
297 Err(e) => {
298 return server_error(e);
299 }
300 };
301 match res {
302 IdentifyUserResponse::WaitForCode => {
303 state = IdentifyUserState::SubmitCode;
304
305 other_id = Some(other_user_id);
306 }
307 IdentifyUserResponse::ProvideCode { step, totp } => {
308 state = IdentifyUserState::DisplayCodeFirst {
309 self_totp: totp,
310 step,
311 };
312
313 other_id = Some(other_user_id);
314 }
315 _ => {
316 eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
317 return;
318 }
319 }
320 }
321 IdentifyUserState::SubmitCode => {
322 let other_totp: String = Input::new()
324 .with_prompt("Insert here the other person code")
325 .validate_with(|s: &String| -> Result<(), &str> {
326 if VALIDATE_TOTP_RE.is_match(s) {
327 Ok(())
328 } else {
329 Err("The code should be a 5 or 6 digit number")
330 }
331 })
332 .interact_text()
333 .expect("Failed to interact with interactive session");
334
335 let res = match &client
336 .idm_person_identify_user(
337 other_id.as_deref().unwrap_or_default(),
338 IdentifyUserRequest::SubmitCode {
339 other_totp: other_totp.parse().unwrap_or_default(),
340 },
341 )
342 .await
343 {
344 Ok(res) => res.clone(),
345 Err(e) => {
346 return server_error(e);
347 }
348 };
349 match res {
350 IdentifyUserResponse::CodeFailure => {
351 eprintln!("{CODE_FAILURE_ERROR_MESSAGE}");
352 return;
353 }
354 IdentifyUserResponse::Success => {
355 println!(
356 "{}'s identity has been successfully verified 🎉🎉",
357 other_id.as_deref().unwrap_or_default()
358 );
359 return;
360 }
361 IdentifyUserResponse::ProvideCode { step, totp } => {
362 state = IdentifyUserState::DisplayCodeSecond {
364 self_totp: totp,
365 step,
366 };
367 }
368
369 _ => {
370 eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
371 return;
372 }
373 }
374 }
375 IdentifyUserState::DisplayCodeFirst { self_totp, step } => {
376 println!("Provide the following code when asked: {self_totp}");
377 let seconds_left = get_ms_left_from_now(step as u128) / 1000;
378 println!("This codes expires in {seconds_left} seconds");
379 let _ = stdout().flush();
380 if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
381 println!("Identity verification failed. Exiting...");
382 return;
383 }
384 match Confirm::new()
385 .with_prompt(format!("Did you confirm that {} correctly verified your code? If you proceed, you won't be able to go back.", other_id.as_deref().unwrap_or_default()))
386 .interact() {
387 Ok(true) => {println!("Code confirmed, continuing...")}
388 Ok(false) => {
389 println!("Identity verification failed. Exiting...");
390 return;
391 },
392 Err(e) => {
393 eprintln!("An error occurred while trying to read from stderr: {e:?}"); println!("Exiting...");
395 return;
396 },
397 };
398
399 state = IdentifyUserState::SubmitCode;
400 }
401 IdentifyUserState::DisplayCodeSecond { self_totp, step } => {
402 println!("Provide the following code when asked: {self_totp}");
403 let seconds_left = get_ms_left_from_now(step as u128) / 1000;
404 println!("This codes expires in {seconds_left} seconds!");
405 let _ = stdout().flush();
406 if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
407 println!("Identity verification failed. Exiting...");
408 return;
409 }
410 match Confirm::new()
411 .with_prompt(format!("Did you confirm that {} correctly verified your code? If you proceed, you won't be able to go back.", other_id.as_deref().unwrap_or_default()))
412 .interact() {
413 Ok(true) => {println!(
414 "{}'s identity has been successfully verified 🎉🎉",
415 other_id.take().unwrap_or_default()
416 );
417 return;}
418 Ok(false) => {
419 println!("Exiting...");
420 return;
421 },
422 Err(e) => {
423 eprintln!("An error occurred while trying to read from stderr: {e:?}"); println!("Exiting...");
425 return;
426 },
427 };
428 }
429 }
430 }
431 }
432
433 fn get_ms_left_from_now(step: u128) -> u32 {
434 #[allow(clippy::expect_used)]
435 let dur = SystemTime::now()
436 .duration_since(SystemTime::UNIX_EPOCH)
437 .expect("invalid duration from epoch now");
438 let ms: u128 = dur.as_millis();
439 (step * 1000 - ms % (step * 1000)) as u32
440 }
441}