1use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier, JwtError};
2use dialoguer::theme::ColorfulTheme;
3use dialoguer::{Confirm, Select};
4use kanidm_client::{KanidmClient, KanidmClientBuilder};
5use kanidm_proto::constants::{DEFAULT_CLIENT_CONFIG_PATH, DEFAULT_CLIENT_CONFIG_PATH_HOME};
6use kanidm_proto::internal::{PrivilegesActive, UserAuthToken};
7use time::format_description::well_known::Rfc3339;
8use time::OffsetDateTime;
9
10use crate::session::{process_auth_state, read_tokens};
11use crate::{KanidmClientParser, LoginOpt};
12
13#[derive(Debug)]
14#[allow(clippy::large_enum_variant)]
15pub enum ToClientError {
16 NeedLogin(String),
17 NeedReauth(String, KanidmClient),
18 ReadOnly,
19 Other,
20}
21
22#[derive(Debug, Copy, Clone)]
25pub(crate) enum OpType {
26 Read,
27 Write,
28}
29
30impl KanidmClientParser {
31 pub fn to_unauth_client(&self) -> KanidmClient {
32 let config_path: String = shellexpand::tilde(DEFAULT_CLIENT_CONFIG_PATH_HOME).into_owned();
33
34 let instance_name: Option<&str> = self.instance.as_deref();
35
36 let client_builder = KanidmClientBuilder::new()
37 .read_options_from_optional_instance_config(DEFAULT_CLIENT_CONFIG_PATH, instance_name)
38 .map_err(|e| {
39 error!(
40 "Failed to parse config ({:?}) -- {:?}",
41 DEFAULT_CLIENT_CONFIG_PATH, e
42 );
43 e
44 })
45 .and_then(|cb| {
46 cb.read_options_from_optional_instance_config(&config_path, instance_name)
47 .map_err(|e| {
48 error!("Failed to parse config ({:?}) -- {:?}", config_path, e);
49 e
50 })
51 })
52 .unwrap_or_else(|_e| {
53 std::process::exit(1);
54 });
55 debug!(
56 "Successfully loaded configuration, looked in {} and {} - client builder state: {:?}",
57 DEFAULT_CLIENT_CONFIG_PATH, DEFAULT_CLIENT_CONFIG_PATH_HOME, &client_builder
58 );
59
60 let client_builder = match &self.addr {
61 Some(a) => client_builder.address(a.to_string()),
62 None => client_builder,
63 };
64
65 let ca_path: Option<&str> = self.ca_path.as_ref().and_then(|p| p.to_str());
66 let client_builder = match ca_path {
67 Some(p) => {
68 debug!("Adding trusted CA cert {:?}", p);
69 let client_builder = client_builder
70 .add_root_certificate_filepath(p)
71 .unwrap_or_else(|e| {
72 error!("Failed to add ca certificate -- {:?}", e);
73 std::process::exit(1);
74 });
75
76 debug!(
77 "After attempting to add trusted CA cert, client builder state: {:?}",
78 client_builder
79 );
80 client_builder
81 }
82 None => client_builder,
83 };
84
85 let client_builder = match self.skip_hostname_verification {
86 true => {
87 warn!(
88 "Accepting invalid hostnames on the certificate for {:?}",
89 &self.addr
90 );
91 client_builder.danger_accept_invalid_hostnames(true)
92 }
93 false => client_builder,
94 };
95
96 let client_builder = match self.accept_invalid_certs {
97 true => {
98 warn!(
99 "TLS Certificate Verification disabled!!! This can lead to credential and account compromise!!!"
100 );
101 client_builder.danger_accept_invalid_certs(true)
102 }
103 false => client_builder,
104 };
105
106 let client_builder = client_builder.set_token_cache_path(self.token_cache_path.clone());
107
108 client_builder.build().unwrap_or_else(|e| {
109 error!("Failed to build client instance -- {:?}", e);
110 std::process::exit(1);
111 })
112 }
113
114 pub(crate) async fn try_to_client(
115 &self,
116 optype: OpType,
117 ) -> Result<KanidmClient, ToClientError> {
118 let client = self.to_unauth_client();
119
120 let token_store = match read_tokens(&client.get_token_cache_path()) {
122 Ok(t) => t,
123 Err(_e) => {
124 error!("Error retrieving authentication token store");
125 return Err(ToClientError::Other);
126 }
127 };
128
129 let Some(token_instance) = token_store.instances(&self.instance) else {
130 error!(
131 "No valid authentication tokens found. Please login with the 'login' subcommand."
132 );
133 return Err(ToClientError::Other);
134 };
135
136 let (spn, jwsc) = match &self.username {
138 Some(filter_username) => {
139 let possible_token = if filter_username.contains('@') {
140 token_instance
142 .tokens()
143 .get(filter_username)
144 .map(|t| (filter_username.clone(), t.clone()))
145 } else {
146 let filter_username_with_hostname = format!(
148 "{}@{}",
149 filter_username,
150 client.get_origin().host_str().unwrap_or("localhost")
151 );
152 debug!(
153 "Looking for tokens matching {}",
154 filter_username_with_hostname
155 );
156
157 let mut token_refs: Vec<_> = token_instance
158 .tokens()
159 .iter()
160 .filter(|(t, _)| *t == &filter_username_with_hostname)
161 .map(|(k, v)| (k.clone(), v.clone()))
162 .collect();
163
164 if token_refs.len() == 1 {
165 token_refs.pop()
167 } else {
168 let filter_username = format!("{filter_username}@");
170 let mut token_refs: Vec<_> = token_instance
172 .tokens()
173 .iter()
174 .filter(|(t, _)| t.starts_with(&filter_username))
175 .map(|(s, j)| (s.clone(), j.clone()))
176 .collect();
177
178 match token_refs.len() {
179 0 => None,
180 1 => token_refs.pop(),
181 _ => {
182 error!("Multiple authentication tokens found for {}. Please specify the full spn to proceed", filter_username);
183 return Err(ToClientError::Other);
184 }
185 }
186 }
187 };
188
189 match possible_token {
191 Some(t) => t,
192 None => {
193 error!(
194 "No valid authentication tokens found for {}.",
195 filter_username
196 );
197 return Err(ToClientError::NeedLogin(filter_username.clone()));
198 }
199 }
200 }
201 None => {
202 if token_instance.tokens().len() == 1 {
203 #[allow(clippy::expect_used)]
204 let (f_uname, f_token) = token_instance
205 .tokens()
206 .iter()
207 .next()
208 .expect("Memory Corruption");
209 debug!("Using cached token for name {}", f_uname);
211 (f_uname.clone(), f_token.clone())
212 } else {
213 match prompt_for_username_get_values(
216 &client.get_token_cache_path(),
217 &self.instance,
218 ) {
219 Ok(tuple) => tuple,
220 Err(msg) => {
221 error!("Error: {}", msg);
222 std::process::exit(1);
223 }
224 }
225 }
226 }
227 };
228
229 let Some(key_id) = jwsc.kid() else {
230 error!("token invalid, not key id associated");
231 return Err(ToClientError::Other);
232 };
233
234 let Some(pub_jwk) = token_instance.keys().get(key_id) else {
235 error!("token invalid, no cached jwk available");
236 return Err(ToClientError::Other);
237 };
238
239 let jws_verifier = match JwsEs256Verifier::try_from(pub_jwk) {
241 Ok(verifier) => verifier,
242 Err(err) => {
243 error!(?err, "Unable to configure jws verifier");
244 return Err(ToClientError::Other);
245 }
246 };
247
248 match jws_verifier.verify(&jwsc).and_then(|jws| {
249 jws.from_json::<UserAuthToken>().map_err(|serde_err| {
250 error!(?serde_err);
251 JwtError::InvalidJwt
252 })
253 }) {
254 Ok(uat) => {
255 #[allow(clippy::disallowed_methods)]
256 let now_utc = time::OffsetDateTime::now_utc();
258 if let Some(exp) = uat.expiry {
259 if now_utc >= exp {
260 error!(
261 "Session has expired for {} - you may need to login again.",
262 uat.spn
263 );
264 return Err(ToClientError::NeedLogin(spn));
265 }
266 }
267
268 client.set_token(jwsc.to_string()).await;
270
271 match optype {
273 OpType::Read => {}
274 OpType::Write => {
275 match uat.purpose_privilege_state(now_utc + time::Duration::seconds(20)) {
276 PrivilegesActive::True => {}
278 PrivilegesActive::ReauthRequired => {
279 error!(
280 "Privileges have expired for {} - you need to re-authenticate again.",
281 uat.spn
282 );
283 return Err(ToClientError::NeedReauth(spn, client));
284 }
285 PrivilegesActive::False => {
286 error!("The current session for {} is read-only.", uat.spn);
287 return Err(ToClientError::ReadOnly);
288 }
289 }
290 }
291 }
292 }
293 Err(e) => {
294 error!("Unable to read token for requested user - you may need to login again.");
295 debug!(?e, "JWT Error");
296 return Err(ToClientError::NeedLogin(spn));
297 }
298 };
299
300 Ok(client)
301 }
302
303 pub(crate) async fn to_client(&self, optype: OpType) -> KanidmClient {
304 loop {
305 match self.try_to_client(optype).await {
306 Ok(c) => break c,
307 Err(ToClientError::NeedLogin(username)) => {
308 if !Confirm::new()
309 .with_prompt("Would you like to login again?")
310 .default(true)
311 .interact()
312 .expect("Failed to interact with interactive session")
313 {
314 std::process::exit(1);
315 }
316
317 let copt = Self {
318 username: Some(username),
319 ..self.to_owned()
320 };
321
322 let login_opt = LoginOpt {};
323
324 login_opt.exec(copt).await;
326 continue;
327 }
328
329 Err(ToClientError::NeedReauth(username, _client)) => {
330 if !Confirm::new()
331 .with_prompt("Would you like to re-authenticate?")
332 .default(true)
333 .interact()
334 .expect("Failed to interact with interactive session")
335 {
336 std::process::exit(1);
337 }
338
339 let copt = Self {
340 username: Some(username),
341 ..self.to_owned()
342 };
343 Box::pin(copt.reauth()).await;
344
345 continue;
347 }
348 Err(ToClientError::ReadOnly) | Err(ToClientError::Other) => {
349 std::process::exit(1);
350 }
351 }
352 }
353 }
354
355 pub(crate) async fn reauth(&self) {
356 let client = self.to_client(OpType::Read).await;
358
359 let allowed = client.reauth_begin().await.unwrap_or_else(|e| {
360 error!("Error during reauthentication begin phase: {:?}", e);
361 std::process::exit(1);
362 });
363
364 process_auth_state(allowed, client, &self.password, &self.instance).await;
365 }
366}
367
368pub fn prompt_for_username_get_values(
372 token_cache_path: &str,
373 instance_name: &Option<String>,
374) -> Result<(String, JwsCompact), String> {
375 let token_store = match read_tokens(token_cache_path) {
376 Ok(value) => value,
377 _ => return Err("Error retrieving authentication token store".to_string()),
378 };
379
380 let Some(token_instance) = token_store.instances(instance_name) else {
381 error!("No tokens in store, quitting!");
382 std::process::exit(1);
383 };
384
385 if token_instance.tokens().is_empty() {
386 error!("No tokens in store, quitting!");
387 std::process::exit(1);
388 }
389 let mut options = Vec::new();
390 for option in token_instance.tokens().iter() {
391 options.push(String::from(option.0));
392 }
393 let user_select = Select::with_theme(&ColorfulTheme::default())
394 .with_prompt("Multiple authentication tokens exist. Please select one")
395 .default(0)
396 .items(&options)
397 .interact();
398 let selection = match user_select {
399 Err(error) => {
400 error!("Failed to handle user input: {:?}", error);
401 std::process::exit(1);
402 }
403 Ok(value) => value,
404 };
405 debug!("Index of the chosen menu item: {:?}", selection);
406
407 match token_instance.tokens().iter().nth(selection) {
408 Some(value) => {
409 let (f_uname, f_token) = value;
410 debug!("Using cached token for name {}", f_uname);
411 debug!(
412 "First ten chars of cached token: {}",
413 &f_token.to_string()[..10]
414 );
415 Ok((f_uname.to_string(), f_token.clone()))
416 }
417 None => {
418 error!("Memory corruption trying to read token store, quitting!");
419 std::process::exit(1);
420 }
421 }
422}
423
424pub fn prompt_for_username_get_username(
428 token_cache_path: &str,
429 instance_name: &Option<String>,
430) -> Result<String, String> {
431 match prompt_for_username_get_values(token_cache_path, instance_name) {
432 Ok(value) => {
433 let (f_user, _) = value;
434 Ok(f_user)
435 }
436 Err(err) => Err(err),
437 }
438}
439
440pub(crate) fn try_expire_at_from_string(input: &str) -> Result<Option<String>, ()> {
460 match input {
461 "any" | "never" | "clear" => Ok(None),
462 "now" => {
463 #[allow(clippy::disallowed_methods)]
464 match OffsetDateTime::now_utc().format(&Rfc3339) {
466 Ok(s) => Ok(Some(s)),
467 Err(e) => {
468 error!(err = ?e, "Unable to format current time to rfc3339");
469 Err(())
470 }
471 }
472 }
473 "epoch" => match OffsetDateTime::UNIX_EPOCH.format(&Rfc3339) {
474 Ok(val) => Ok(Some(val)),
475 Err(err) => {
476 error!("Failed to format epoch timestamp as RFC3339: {:?}", err);
477 Err(())
478 }
479 },
480 _ => {
481 match OffsetDateTime::parse(input, &Rfc3339) {
483 Ok(_) => Ok(Some(input.to_string())),
484 Err(err) => {
485 error!("Failed to parse supplied timestamp: {:?}", err);
486 Err(())
487 }
488 }
489 }
490 }
491}