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 kanidm_proto::cli::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 async fn exec(&self, opt: KanidmClientParser) {
90 match self {
91 SelfOpt::Whoami => {
92 let client = opt.to_client(OpType::Read).await;
93
94 match client.whoami().await {
95 Ok(o_ent) => {
96 match o_ent {
97 Some(ent) => {
98 opt.output_mode.print_message(ent);
99 }
100 None => {
101 error!("Authentication with cached token failed, can't query information.");
102 }
104 }
105 }
106 Err(e) => handle_client_error(e, opt.output_mode),
107 }
108 }
109 SelfOpt::IdentifyUser => {
110 let client = opt.to_client(OpType::Write).await;
111 let whoami_response = match client.whoami().await {
112 Ok(o_ent) => {
113 match o_ent {
114 Some(ent) => ent,
115 None => {
116 eprintln!("Authentication with cached token failed, can't query information."); return;
118 }
119 }
120 }
121 Err(e) => {
122 opt.output_mode
123 .print_message(format!("Error querying whoami endpoint: {e:?}")); return;
125 }
126 };
127
128 let spn =
129 match whoami_response.attrs.get("spn").and_then(|v| v.first()) {
130 Some(spn) => spn,
131 None => {
132 opt.output_mode.print_message("Failed to parse your SPN from the system's whoami endpoint, exiting!"); return;
134 }
135 };
136
137 run_identity_verification_no_tui(IdentifyUserState::Start, client, spn, None).await;
138 } }
140 }
141}
142
143impl SystemOpt {
144 pub async fn exec(&self, opt: KanidmClientParser) {
145 match self {
146 SystemOpt::Api { commands } => commands.exec(opt).await,
147 SystemOpt::PwBadlist { commands } => commands.exec(opt).await,
148 SystemOpt::DeniedNames { commands } => commands.exec(opt).await,
149 SystemOpt::Oauth2 { commands } => commands.exec(opt).await,
150 SystemOpt::Domain { commands } => commands.exec(opt).await,
151 SystemOpt::Message { commands } => commands.exec(opt).await,
152 SystemOpt::Synch { commands } => commands.exec(opt).await,
153 }
154 }
155}
156
157impl KanidmClientParser {
158 pub async fn exec(self) {
159 match self.commands.clone() {
160 KanidmClientOpt::Raw { commands } => commands.exec(self).await,
161 KanidmClientOpt::Login(lopt) => lopt.exec(self).await,
162 KanidmClientOpt::Reauth { mode } => self.reauth(mode).await,
163 KanidmClientOpt::Logout(lopt) => lopt.exec(self).await,
164 KanidmClientOpt::Session { commands } => commands.exec(self).await,
165 KanidmClientOpt::CSelf { commands } => commands.exec(self).await,
166 KanidmClientOpt::Person { commands } => commands.exec(self).await,
167 KanidmClientOpt::ServiceAccount { commands } => commands.exec(self).await,
168 KanidmClientOpt::Group { commands } => commands.exec(self).await,
169 KanidmClientOpt::Graph(gops) => gops.exec(self).await,
170 KanidmClientOpt::System { commands } => commands.exec(self).await,
171 KanidmClientOpt::Schema {
172 commands: SchemaOpt::Class { commands },
173 } => commands.exec(self).await,
174 KanidmClientOpt::Schema {
175 commands: SchemaOpt::Attribute { commands },
176 } => commands.exec(self).await,
177 KanidmClientOpt::Recycle { commands } => commands.exec(self).await,
178 KanidmClientOpt::Version => {
179 self.output_mode
180 .print_message(format!("kanidm {}", env!("KANIDM_PKG_VERSION")));
181 }
182 }
183 }
184}
185
186pub(crate) fn password_prompt(prompt: &str) -> Option<String> {
187 for _ in 0..3 {
188 let password = dialoguer::Password::new()
189 .with_prompt(prompt)
190 .interact()
191 .ok()?;
192
193 let password_confirm = dialoguer::Password::new()
194 .with_prompt("Reenter the password to confirm: ")
195 .interact()
196 .ok()?;
197
198 if password == password_confirm {
199 return Some(password);
200 } else {
201 error!("Passwords do not match");
202 }
203 }
204 None
205}
206
207pub const IDENTITY_UNAVAILABLE_ERROR_MESSAGE: &str = "Identity verification is not available.";
208pub 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!";
209pub const INVALID_STATE_ERROR_MESSAGE: &str =
210 "The user identification flow is in an invalid state 😵😵";
211
212mod identify_user_no_tui {
213 use crate::{
214 CODE_FAILURE_ERROR_MESSAGE, IDENTITY_UNAVAILABLE_ERROR_MESSAGE, INVALID_STATE_ERROR_MESSAGE,
215 };
216
217 use kanidm_client::{ClientError, KanidmClient};
218 use kanidm_proto::internal::{IdentifyUserRequest, IdentifyUserResponse};
219
220 use dialoguer::{Confirm, Input};
221 use regex::Regex;
222 use std::{
223 io::{stdout, Write},
224 time::SystemTime,
225 };
226
227 lazy_static::lazy_static! {
228 pub static ref VALIDATE_TOTP_RE: Regex = {
229 #[allow(clippy::expect_used)]
230 Regex::new(r"^\d{5,6}$").expect("Failed to parse VALIDATE_TOTP_RE") };
232 }
233
234 pub(super) enum IdentifyUserState {
235 Start,
236 IdDisplayAndSubmit,
237 SubmitCode,
238 DisplayCodeFirst { self_totp: u32, step: u32 },
239 DisplayCodeSecond { self_totp: u32, step: u32 },
240 }
241
242 fn server_error(e: &ClientError) {
243 eprintln!("Server error!"); eprintln!("{e:?}");
245 println!("Exiting...");
246 }
247
248 pub(super) async fn run_identity_verification_no_tui(
249 mut state: IdentifyUserState,
250 client: KanidmClient,
251 self_id: &str,
252 mut other_id: Option<String>,
253 ) {
254 loop {
255 match state {
256 IdentifyUserState::Start => {
257 let res = match &client
258 .idm_person_identify_user(self_id, IdentifyUserRequest::Start)
259 .await
260 {
261 Ok(res) => res.clone(),
262 Err(e) => {
263 return server_error(e);
264 }
265 };
266 match res {
267 IdentifyUserResponse::IdentityVerificationUnavailable => {
268 println!("{IDENTITY_UNAVAILABLE_ERROR_MESSAGE}");
269 return;
270 }
271 IdentifyUserResponse::IdentityVerificationAvailable => {
272 state = IdentifyUserState::IdDisplayAndSubmit;
273 }
274 _ => {
275 eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
276 return;
277 }
278 }
279 }
280 IdentifyUserState::IdDisplayAndSubmit => {
281 println!("When asked for your ID, provide the following: {self_id}");
282
283 let other_user_id: String = Input::new()
285 .with_prompt("Ask for the other person's ID, and insert it here")
286 .interact_text()
287 .expect("Failed to interact with interactive session");
288 let _ = stdout().flush();
289
290 let res = match &client
291 .idm_person_identify_user(&other_user_id, IdentifyUserRequest::Start)
292 .await
293 {
294 Ok(res) => res.clone(),
295 Err(e) => {
296 return server_error(e);
297 }
298 };
299 match res {
300 IdentifyUserResponse::WaitForCode => {
301 state = IdentifyUserState::SubmitCode;
302
303 other_id = Some(other_user_id);
304 }
305 IdentifyUserResponse::ProvideCode { step, totp } => {
306 state = IdentifyUserState::DisplayCodeFirst {
307 self_totp: totp,
308 step,
309 };
310
311 other_id = Some(other_user_id);
312 }
313 _ => {
314 eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
315 return;
316 }
317 }
318 }
319 IdentifyUserState::SubmitCode => {
320 let other_totp: String = Input::new()
322 .with_prompt("Insert here the other person code")
323 .validate_with(|s: &String| -> Result<(), &str> {
324 if VALIDATE_TOTP_RE.is_match(s) {
325 Ok(())
326 } else {
327 Err("The code should be a 5 or 6 digit number")
328 }
329 })
330 .interact_text()
331 .expect("Failed to interact with interactive session");
332
333 let res = match &client
334 .idm_person_identify_user(
335 other_id.as_deref().unwrap_or_default(),
336 IdentifyUserRequest::SubmitCode {
337 other_totp: other_totp.parse().unwrap_or_default(),
338 },
339 )
340 .await
341 {
342 Ok(res) => res.clone(),
343 Err(e) => {
344 return server_error(e);
345 }
346 };
347 match res {
348 IdentifyUserResponse::CodeFailure => {
349 eprintln!("{CODE_FAILURE_ERROR_MESSAGE}");
350 return;
351 }
352 IdentifyUserResponse::Success => {
353 println!(
354 "{}'s identity has been successfully verified 🎉🎉",
355 other_id.as_deref().unwrap_or_default()
356 );
357 return;
358 }
359 IdentifyUserResponse::ProvideCode { step, totp } => {
360 state = IdentifyUserState::DisplayCodeSecond {
362 self_totp: totp,
363 step,
364 };
365 }
366
367 _ => {
368 eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
369 return;
370 }
371 }
372 }
373 IdentifyUserState::DisplayCodeFirst { self_totp, step } => {
374 println!("Provide the following code when asked: {self_totp}");
375 let seconds_left = get_ms_left_from_now(step as u128) / 1000;
376 println!("This codes expires in {seconds_left} seconds");
377 let _ = stdout().flush();
378 if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
379 println!("Identity verification failed. Exiting...");
380 return;
381 }
382 match Confirm::new()
383 .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()))
384 .interact() {
385 Ok(true) => {println!("Code confirmed, continuing...")}
386 Ok(false) => {
387 println!("Identity verification failed. Exiting...");
388 return;
389 },
390 Err(e) => {
391 eprintln!("An error occurred while trying to read from stderr: {e:?}"); println!("Exiting...");
393 return;
394 },
395 };
396
397 state = IdentifyUserState::SubmitCode;
398 }
399 IdentifyUserState::DisplayCodeSecond { self_totp, step } => {
400 println!("Provide the following code when asked: {self_totp}");
401 let seconds_left = get_ms_left_from_now(step as u128) / 1000;
402 println!("This codes expires in {seconds_left} seconds!");
403 let _ = stdout().flush();
404 if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
405 println!("Identity verification failed. Exiting...");
406 return;
407 }
408 match Confirm::new()
409 .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()))
410 .interact() {
411 Ok(true) => {println!(
412 "{}'s identity has been successfully verified 🎉🎉",
413 other_id.take().unwrap_or_default()
414 );
415 return;}
416 Ok(false) => {
417 println!("Exiting...");
418 return;
419 },
420 Err(e) => {
421 eprintln!("An error occurred while trying to read from stderr: {e:?}"); println!("Exiting...");
423 return;
424 },
425 };
426 }
427 }
428 }
429 }
430
431 fn get_ms_left_from_now(step: u128) -> u32 {
432 #[allow(clippy::expect_used)]
433 let dur = SystemTime::now()
434 .duration_since(SystemTime::UNIX_EPOCH)
435 .expect("invalid duration from epoch now");
436 let ms: u128 = dur.as_millis();
437 (step * 1000 - ms % (step * 1000)) as u32
438 }
439}