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 client_builder.build().unwrap_or_else(|e| {
95 error!("Failed to build client instance -- {:?}", e);
96 std::process::exit(1);
97 })
98 }
99
100 pub(crate) async fn try_to_client(
101 &self,
102 optype: OpType,
103 ) -> Result<KanidmClient, ToClientError> {
104 let client = self.to_unauth_client();
105
106 let token_store = match read_tokens(&client.get_token_cache_path()) {
108 Ok(t) => t,
109 Err(_e) => {
110 error!("Error retrieving authentication token store");
111 return Err(ToClientError::Other);
112 }
113 };
114
115 let Some(token_instance) = token_store.instances(&self.instance) else {
116 error!(
117 "No valid authentication tokens found. Please login with the 'login' subcommand."
118 );
119 return Err(ToClientError::Other);
120 };
121
122 let (spn, jwsc) = match &self.username {
124 Some(filter_username) => {
125 let possible_token = if filter_username.contains('@') {
126 token_instance
128 .tokens()
129 .get(filter_username)
130 .map(|t| (filter_username.clone(), t.clone()))
131 } else {
132 let filter_username_with_hostname = format!(
134 "{}@{}",
135 filter_username,
136 client.get_origin().host_str().unwrap_or("localhost")
137 );
138 debug!(
139 "Looking for tokens matching {}",
140 filter_username_with_hostname
141 );
142
143 let mut token_refs: Vec<_> = token_instance
144 .tokens()
145 .iter()
146 .filter(|(t, _)| *t == &filter_username_with_hostname)
147 .map(|(k, v)| (k.clone(), v.clone()))
148 .collect();
149
150 if token_refs.len() == 1 {
151 token_refs.pop()
153 } else {
154 let filter_username = format!("{}@", filter_username);
156 let mut token_refs: Vec<_> = token_instance
158 .tokens()
159 .iter()
160 .filter(|(t, _)| t.starts_with(&filter_username))
161 .map(|(s, j)| (s.clone(), j.clone()))
162 .collect();
163
164 match token_refs.len() {
165 0 => None,
166 1 => token_refs.pop(),
167 _ => {
168 error!("Multiple authentication tokens found for {}. Please specify the full spn to proceed", filter_username);
169 return Err(ToClientError::Other);
170 }
171 }
172 }
173 };
174
175 match possible_token {
177 Some(t) => t,
178 None => {
179 error!(
180 "No valid authentication tokens found for {}.",
181 filter_username
182 );
183 return Err(ToClientError::NeedLogin(filter_username.clone()));
184 }
185 }
186 }
187 None => {
188 if token_instance.tokens().len() == 1 {
189 #[allow(clippy::expect_used)]
190 let (f_uname, f_token) = token_instance
191 .tokens()
192 .iter()
193 .next()
194 .expect("Memory Corruption");
195 debug!("Using cached token for name {}", f_uname);
197 (f_uname.clone(), f_token.clone())
198 } else {
199 match prompt_for_username_get_values(
202 &client.get_token_cache_path(),
203 &self.instance,
204 ) {
205 Ok(tuple) => tuple,
206 Err(msg) => {
207 error!("Error: {}", msg);
208 std::process::exit(1);
209 }
210 }
211 }
212 }
213 };
214
215 let Some(key_id) = jwsc.kid() else {
216 error!("token invalid, not key id associated");
217 return Err(ToClientError::Other);
218 };
219
220 let Some(pub_jwk) = token_instance.keys().get(key_id) else {
221 error!("token invalid, no cached jwk available");
222 return Err(ToClientError::Other);
223 };
224
225 let jws_verifier = match JwsEs256Verifier::try_from(pub_jwk) {
227 Ok(verifier) => verifier,
228 Err(err) => {
229 error!(?err, "Unable to configure jws verifier");
230 return Err(ToClientError::Other);
231 }
232 };
233
234 match jws_verifier.verify(&jwsc).and_then(|jws| {
235 jws.from_json::<UserAuthToken>().map_err(|serde_err| {
236 error!(?serde_err);
237 JwtError::InvalidJwt
238 })
239 }) {
240 Ok(uat) => {
241 let now_utc = time::OffsetDateTime::now_utc();
242 if let Some(exp) = uat.expiry {
243 if now_utc >= exp {
244 error!(
245 "Session has expired for {} - you may need to login again.",
246 uat.spn
247 );
248 return Err(ToClientError::NeedLogin(spn));
249 }
250 }
251
252 client.set_token(jwsc.to_string()).await;
254
255 match optype {
257 OpType::Read => {}
258 OpType::Write => {
259 if !uat.purpose_readwrite_active(now_utc + time::Duration::new(20, 0)) {
260 error!(
261 "Privileges have expired for {} - you need to re-authenticate again.",
262 uat.spn
263 );
264 return Err(ToClientError::NeedReauth(spn, client));
265 }
266 }
267 }
268 }
269 Err(e) => {
270 error!("Unable to read token for requested user - you may need to login again.");
271 debug!(?e, "JWT Error");
272 return Err(ToClientError::NeedLogin(spn));
273 }
274 };
275
276 Ok(client)
277 }
278
279 pub async fn to_client(&self, optype: OpType) -> KanidmClient {
280 let mut copt_mut = self.clone();
281 loop {
282 match self.try_to_client(optype.clone()).await {
283 Ok(c) => break c,
284 Err(ToClientError::NeedLogin(username)) => {
285 if !Confirm::new()
286 .with_prompt("Would you like to login again?")
287 .default(true)
288 .interact()
289 .expect("Failed to interact with interactive session")
290 {
291 std::process::exit(1);
292 }
293
294 copt_mut.username = Some(username);
295 let copt = copt_mut.clone();
296 let login_opt = LoginOpt {
297 copt,
298 password: env::var("KANIDM_PASSWORD").ok(),
299 };
300
301 login_opt.exec().await;
302 continue;
304 }
305 Err(ToClientError::NeedReauth(username, client)) => {
306 if !Confirm::new()
307 .with_prompt("Would you like to re-authenticate?")
308 .default(true)
309 .interact()
310 .expect("Failed to interact with interactive session")
311 {
312 std::process::exit(1);
313 }
314 copt_mut.username = Some(username);
315 let copt = copt_mut.clone();
316 let reauth_opt = ReauthOpt { copt };
317 reauth_opt.inner(client).await;
318
319 continue;
321 }
322 Err(ToClientError::Other) => {
323 std::process::exit(1);
324 }
325 }
326 }
327 }
328}
329
330pub fn prompt_for_username_get_values(
334 token_cache_path: &str,
335 instance_name: &Option<String>,
336) -> Result<(String, JwsCompact), String> {
337 let token_store = match read_tokens(token_cache_path) {
338 Ok(value) => value,
339 _ => return Err("Error retrieving authentication token store".to_string()),
340 };
341
342 let Some(token_instance) = token_store.instances(instance_name) else {
343 error!("No tokens in store, quitting!");
344 std::process::exit(1);
345 };
346
347 if token_instance.tokens().is_empty() {
348 error!("No tokens in store, quitting!");
349 std::process::exit(1);
350 }
351 let mut options = Vec::new();
352 for option in token_instance.tokens().iter() {
353 options.push(String::from(option.0));
354 }
355 let user_select = Select::with_theme(&ColorfulTheme::default())
356 .with_prompt("Multiple authentication tokens exist. Please select one")
357 .default(0)
358 .items(&options)
359 .interact();
360 let selection = match user_select {
361 Err(error) => {
362 error!("Failed to handle user input: {:?}", error);
363 std::process::exit(1);
364 }
365 Ok(value) => value,
366 };
367 debug!("Index of the chosen menu item: {:?}", selection);
368
369 match token_instance.tokens().iter().nth(selection) {
370 Some(value) => {
371 let (f_uname, f_token) = value;
372 debug!("Using cached token for name {}", f_uname);
373 debug!("Cached token: {}", f_token);
374 Ok((f_uname.to_string(), f_token.clone()))
375 }
376 None => {
377 error!("Memory corruption trying to read token store, quitting!");
378 std::process::exit(1);
379 }
380 }
381}
382
383pub fn prompt_for_username_get_username(
387 token_cache_path: &str,
388 instance_name: &Option<String>,
389) -> Result<String, String> {
390 match prompt_for_username_get_values(token_cache_path, instance_name) {
391 Ok(value) => {
392 let (f_user, _) = value;
393 Ok(f_user)
394 }
395 Err(err) => Err(err),
396 }
397}
398
399pub(crate) fn try_expire_at_from_string(input: &str) -> Result<Option<String>, ()> {
419 match input {
420 "any" | "never" | "clear" => Ok(None),
421 "now" => match OffsetDateTime::now_utc().format(&Rfc3339) {
422 Ok(s) => Ok(Some(s)),
423 Err(e) => {
424 error!(err = ?e, "Unable to format current time to rfc3339");
425 Err(())
426 }
427 },
428 "epoch" => match OffsetDateTime::UNIX_EPOCH.format(&Rfc3339) {
429 Ok(val) => Ok(Some(val)),
430 Err(err) => {
431 error!("Failed to format epoch timestamp as RFC3339: {:?}", err);
432 Err(())
433 }
434 },
435 _ => {
436 match OffsetDateTime::parse(input, &Rfc3339) {
438 Ok(_) => Ok(Some(input.to_string())),
439 Err(err) => {
440 error!("Failed to parse supplied timestamp: {:?}", err);
441 Err(())
442 }
443 }
444 }
445 }
446}