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}