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