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