Skip to main content

kasl/api/
mod.rs

1//! External API clients (GitLab, Jira, SiServer, kasl-server) and the shared
2//! session-management pattern most of them implement.
3//!
4//! [`kasl_server`] is the exception: it authenticates with a token an
5//! administrator issues, so it has no session to manage and does not
6//! implement [`Session`].
7//!
8//! ```text
9//! use kasl::api::{GitLabConfig, JiraConfig, SiConfig};
10//!
11//! let jira_module = JiraConfig::module();
12//! let jira_config = JiraConfig::init(&existing_config)?;
13//! ```
14
15use crate::libs::messages::Message;
16use crate::libs::{data_storage::DataStorage, secret::Secret};
17use crate::msg_error_anyhow;
18use anyhow::Result;
19use std::fs;
20use std::io::Write;
21
22pub mod gitlab;
23pub mod jira;
24pub mod kasl_server;
25pub mod si;
26
27pub use gitlab::GitLabConfig;
28pub use jira::JiraConfig;
29pub use si::SiConfig;
30
31/// Authentication attempts before giving up on a password.
32const MAX_RETRY_COUNT: i32 = 3;
33
34/// Session lifecycle shared by every API client: cache the session id on
35/// disk, prompt for the password only when needed, retry a bounded number
36/// of times.
37///
38/// Implementors supply the provider-specific pieces (`login`,
39/// `set_credentials`, file names); the provided methods do the rest.
40#[allow(async_fn_in_trait)]
41pub trait Session {
42    /// Authenticates with the stored credentials, returning a session id.
43    async fn login(&self) -> Result<String>;
44
45    /// Stores the password (encoded as the provider requires) for `login`.
46    fn set_credentials(&mut self, password: &str) -> Result<()>;
47
48    /// Per-provider session cache filename.
49    fn session_id_file(&self) -> &str;
50
51    /// Per-provider secret manager (prompt text, cache file).
52    fn secret(&self) -> Secret;
53
54    /// Current failed-attempt count.
55    fn retry(&self) -> i32;
56
57    /// Bumps the failed-attempt count.
58    fn inc_retry(&mut self);
59
60    /// Clears the failed-attempt count after a successful login.
61    fn reset_retry(&mut self);
62
63    /// Returns a session id: cached if available, otherwise via login with
64    /// up to [`MAX_RETRY_COUNT`] password attempts (a retry always
65    /// re-prompts rather than reusing a password that just failed).
66    async fn get_session_id(&mut self) -> Result<String> {
67        let session_id_file_path = DataStorage::new().get_path(self.session_id_file())?;
68        let session_id_file_path_str = session_id_file_path.to_str().unwrap();
69
70        if let Ok(session_id) = Self::read_session_id(session_id_file_path_str) {
71            Ok(session_id)
72        } else {
73            loop {
74                let password: String = match self.retry() > 0 {
75                    true => self.secret().prompt()?,         // Force new prompt on retry
76                    false => self.secret().get_or_prompt()?, // Use cache if available
77                };
78
79                self.set_credentials(&password)?;
80
81                let session_id = self.login().await;
82                match session_id {
83                    Ok(session_id) => {
84                        let _ = Self::write_session_id(session_id_file_path_str, &session_id);
85                        self.reset_retry();
86                        return Ok(session_id);
87                    }
88                    Err(_) => {
89                        if self.retry() < MAX_RETRY_COUNT {
90                            self.inc_retry();
91                            continue;
92                        }
93                        break Err(msg_error_anyhow!(Message::WrongPassword(MAX_RETRY_COUNT)));
94                    }
95                }
96            }
97        }
98    }
99
100    /// Reads the cached session id.
101    fn read_session_id(file_name: &str) -> Result<String> {
102        Ok(fs::read_to_string(file_name)?)
103    }
104
105    /// Writes the session id to the cache file.
106    fn write_session_id(file_name: &str, session_id: &str) -> Result<()> {
107        let mut file = fs::OpenOptions::new().write(true).create(true).truncate(true).open(file_name)?;
108        file.write_all(session_id.as_bytes())?;
109        Ok(())
110    }
111
112    /// Drops the cached session, forcing a fresh login next time.
113    fn delete_session_id(&self) -> Result<()> {
114        let session_id_file_path = DataStorage::new().get_path(self.session_id_file())?;
115        fs::remove_file(session_id_file_path)?;
116        Ok(())
117    }
118}