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