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