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 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 { mode } => self.reauth(mode).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 time::SystemTime,
228 };
229
230 lazy_static::lazy_static! {
231 pub static ref VALIDATE_TOTP_RE: Regex = {
232 #[allow(clippy::expect_used)]
233 Regex::new(r"^\d{5,6}$").expect("Failed to parse VALIDATE_TOTP_RE") };
235 }
236
237 pub(super) enum IdentifyUserState {
238 Start,
239 IdDisplayAndSubmit,
240 SubmitCode,
241 DisplayCodeFirst { self_totp: u32, step: u32 },
242 DisplayCodeSecond { self_totp: u32, step: u32 },
243 }
244
245 fn server_error(e: &ClientError) {
246 eprintln!("Server error!"); eprintln!("{e:?}");
248 println!("Exiting...");
249 }
250
251 pub(super) async fn run_identity_verification_no_tui(
252 mut state: IdentifyUserState,
253 client: KanidmClient,
254 self_id: &str,
255 mut other_id: Option<String>,
256 ) {
257 loop {
258 match state {
259 IdentifyUserState::Start => {
260 let res = match &client
261 .idm_person_identify_user(self_id, IdentifyUserRequest::Start)
262 .await
263 {
264 Ok(res) => res.clone(),
265 Err(e) => {
266 return server_error(e);
267 }
268 };
269 match res {
270 IdentifyUserResponse::IdentityVerificationUnavailable => {
271 println!("{IDENTITY_UNAVAILABLE_ERROR_MESSAGE}");
272 return;
273 }
274 IdentifyUserResponse::IdentityVerificationAvailable => {
275 state = IdentifyUserState::IdDisplayAndSubmit;
276 }
277 _ => {
278 eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
279 return;
280 }
281 }
282 }
283 IdentifyUserState::IdDisplayAndSubmit => {
284 println!("When asked for your ID, provide the following: {self_id}");
285
286 let other_user_id: String = Input::new()
288 .with_prompt("Ask for the other person's ID, and insert it here")
289 .interact_text()
290 .expect("Failed to interact with interactive session");
291 let _ = stdout().flush();
292
293 let res = match &client
294 .idm_person_identify_user(&other_user_id, IdentifyUserRequest::Start)
295 .await
296 {
297 Ok(res) => res.clone(),
298 Err(e) => {
299 return server_error(e);
300 }
301 };
302 match res {
303 IdentifyUserResponse::WaitForCode => {
304 state = IdentifyUserState::SubmitCode;
305
306 other_id = Some(other_user_id);
307 }
308 IdentifyUserResponse::ProvideCode { step, totp } => {
309 state = IdentifyUserState::DisplayCodeFirst {
310 self_totp: totp,
311 step,
312 };
313
314 other_id = Some(other_user_id);
315 }
316 _ => {
317 eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
318 return;
319 }
320 }
321 }
322 IdentifyUserState::SubmitCode => {
323 let other_totp: String = Input::new()
325 .with_prompt("Insert here the other person code")
326 .validate_with(|s: &String| -> Result<(), &str> {
327 if VALIDATE_TOTP_RE.is_match(s) {
328 Ok(())
329 } else {
330 Err("The code should be a 5 or 6 digit number")
331 }
332 })
333 .interact_text()
334 .expect("Failed to interact with interactive session");
335
336 let res = match &client
337 .idm_person_identify_user(
338 other_id.as_deref().unwrap_or_default(),
339 IdentifyUserRequest::SubmitCode {
340 other_totp: other_totp.parse().unwrap_or_default(),
341 },
342 )
343 .await
344 {
345 Ok(res) => res.clone(),
346 Err(e) => {
347 return server_error(e);
348 }
349 };
350 match res {
351 IdentifyUserResponse::CodeFailure => {
352 eprintln!("{CODE_FAILURE_ERROR_MESSAGE}");
353 return;
354 }
355 IdentifyUserResponse::Success => {
356 println!(
357 "{}'s identity has been successfully verified 🎉🎉",
358 other_id.as_deref().unwrap_or_default()
359 );
360 return;
361 }
362 IdentifyUserResponse::ProvideCode { step, totp } => {
363 state = IdentifyUserState::DisplayCodeSecond {
365 self_totp: totp,
366 step,
367 };
368 }
369
370 _ => {
371 eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
372 return;
373 }
374 }
375 }
376 IdentifyUserState::DisplayCodeFirst { self_totp, step } => {
377 println!("Provide the following code when asked: {self_totp}");
378 let seconds_left = get_ms_left_from_now(step as u128) / 1000;
379 println!("This codes expires in {seconds_left} seconds");
380 let _ = stdout().flush();
381 if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
382 println!("Identity verification failed. Exiting...");
383 return;
384 }
385 match Confirm::new()
386 .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()))
387 .interact() {
388 Ok(true) => {println!("Code confirmed, continuing...")}
389 Ok(false) => {
390 println!("Identity verification failed. Exiting...");
391 return;
392 },
393 Err(e) => {
394 eprintln!("An error occurred while trying to read from stderr: {e:?}"); println!("Exiting...");
396 return;
397 },
398 };
399
400 state = IdentifyUserState::SubmitCode;
401 }
402 IdentifyUserState::DisplayCodeSecond { self_totp, step } => {
403 println!("Provide the following code when asked: {self_totp}");
404 let seconds_left = get_ms_left_from_now(step as u128) / 1000;
405 println!("This codes expires in {seconds_left} seconds!");
406 let _ = stdout().flush();
407 if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
408 println!("Identity verification failed. Exiting...");
409 return;
410 }
411 match Confirm::new()
412 .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()))
413 .interact() {
414 Ok(true) => {println!(
415 "{}'s identity has been successfully verified 🎉🎉",
416 other_id.take().unwrap_or_default()
417 );
418 return;}
419 Ok(false) => {
420 println!("Exiting...");
421 return;
422 },
423 Err(e) => {
424 eprintln!("An error occurred while trying to read from stderr: {e:?}"); println!("Exiting...");
426 return;
427 },
428 };
429 }
430 }
431 }
432 }
433
434 fn get_ms_left_from_now(step: u128) -> u32 {
435 #[allow(clippy::expect_used)]
436 let dur = SystemTime::now()
437 .duration_since(SystemTime::UNIX_EPOCH)
438 .expect("invalid duration from epoch now");
439 let ms: u128 = dur.as_millis();
440 (step * 1000 - ms % (step * 1000)) as u32
441 }
442}