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