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