use crate::login::ui::UiUrl;
use chris::types::{CubeUrl, PluginInstanceId, Username};
use color_eyre::eyre::{Result, WrapErr};
use color_eyre::owo_colors::OwoColorize;
use color_eyre::Section;
use serde::{Deserialize, Serialize};
pub enum Backend {
ClearText,
Keyring,
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
#[serde(tag = "store", content = "value")]
pub enum StoredToken {
Text(String),
Keyring,
None,
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
pub struct SavedCubeState {
pub cube: CubeUrl,
pub username: Username,
pub store: StoredToken,
pub current_plugin_instance_id: Option<PluginInstanceId>,
pub ui: Option<UiUrl>,
}
impl SavedCubeState {
pub fn into_login(self, service: &str) -> Result<CubeState> {
let token = match &self.store {
StoredToken::Keyring => {
let entry = keyring::Entry::new(service, &self.to_keyring_username())?;
let token = entry.get_password()?;
Ok::<_, keyring::Error>(Some(token))
}
StoredToken::Text(token) => Ok(Some(token.to_owned())),
StoredToken::None => Ok(None),
}?;
Ok(CubeState {
cube: self.cube,
username: self.username,
token,
current_plugin_instance_id: self.current_plugin_instance_id,
ui: self.ui,
})
}
fn to_keyring_username(&self) -> String {
format!("{}@{}", self.username.as_str(), self.cube.as_str())
}
}
#[derive(Eq, PartialEq, Debug)]
pub struct CubeState {
pub cube: CubeUrl,
pub username: Username,
pub token: Option<String>,
pub current_plugin_instance_id: Option<PluginInstanceId>,
pub ui: Option<UiUrl>,
}
impl CubeState {
pub fn into_saved(self, backend: Backend, service: &str) -> Result<SavedCubeState> {
let token: StoredToken = if let Some(token) = &self.token {
match backend {
Backend::ClearText => StoredToken::Text(token.to_string()),
Backend::Keyring => {
let entry = keyring::Entry::new(service, &self.to_keyring_username())?;
entry
.set_password(token)
.wrap_err("Could not save token to keyring.")
.with_suggestion(|| {
format!(
"Try using the {} option. \n\n\t{}",
"--no-keyring".bold(),
format!(
"chrs login --cube={} --username={} --token={} --no-keyring",
&self.cube, &self.username, token
)
.bold()
)
})?;
StoredToken::Keyring
}
}
} else {
StoredToken::None
};
let saved = SavedCubeState {
username: self.username,
cube: self.cube,
store: token,
current_plugin_instance_id: self.current_plugin_instance_id,
ui: self.ui,
};
Ok(saved)
}
fn to_keyring_username(&self) -> String {
format!("{}@{}", self.username.as_str(), self.cube.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::*;
use std::str::FromStr;
const TEST_SERVICE: &str = "org.chrisproject.chrs.test";
#[rstest]
fn test_into_login_from_text(cube_url: CubeUrl, username: Username) -> Result<()> {
let token = "my-secret-token";
let (expected, actual) = login_helper(
&cube_url,
&username,
StoredToken::Text(token.to_string()),
&token,
)?;
assert_eq!(expected, actual);
Ok(())
}
#[fixture]
fn username() -> Username {
Username::from_static("chrs-testuser")
}
#[fixture]
fn cube_url() -> CubeUrl {
CubeUrl::from_static("https://example.org/api/v1/")
}
fn login_helper(
cube_url: &CubeUrl,
username: &Username,
stored_token: StoredToken,
actual_token: &str,
) -> Result<(CubeState, CubeState)> {
let cube = SavedCubeState {
cube: cube_url.clone(),
username: username.clone(),
store: stored_token,
current_plugin_instance_id: None,
ui: None,
};
let login = CubeState {
cube: cube_url.clone(),
username: username.clone(),
token: Some(actual_token.to_string()),
current_plugin_instance_id: None,
ui: None,
};
Ok((login, cube.into_login(TEST_SERVICE)?))
}
#[rstest]
fn test_into_login_from_keyring(username: Username, cube_url: CubeUrl) -> Result<()> {
let token = "my-secret-secure-token";
let keyring_username = format!("{}@{}", username.as_str(), cube_url.as_str());
let entry = keyring::Entry::new(TEST_SERVICE, &*keyring_username)?;
entry.set_password(&token)?;
let (expected, actual) = login_helper(&cube_url, &username, StoredToken::Keyring, token)?;
entry.delete_password()?;
assert_eq!(expected, actual);
Ok(())
}
#[test]
fn test_into_saved_with_keyring() -> Result<()> {
let token = "my-secret-secure-token-again";
let address = CubeUrl::from_static("https://another.example.com/api/v1/");
let username = Username::from_str("testing-chrs").unwrap();
let login = CubeState {
cube: address.to_owned(),
username: username.to_owned(),
token: Some(token.to_string()),
current_plugin_instance_id: None,
ui: None,
};
login.into_saved(Backend::Keyring, TEST_SERVICE)?;
let keyring_username = format!("{}@{}", username.as_str(), address.as_str());
let entry = keyring::Entry::new(TEST_SERVICE, &*keyring_username)?;
assert_eq!(token, entry.get_password()?);
entry.delete_password()?;
Ok(())
}
}