use crate::api::auth;
use crate::config::{api_url, build_config};
use crate::error::Error;
use chrono::{DateTime, Utc};
use eyre::{Context, OptionExt};
use keyring::Entry;
use reqwest::StatusCode;
use serde_json::json;
use std::path::{Path, PathBuf};
use users::{get_current_uid, get_user_by_uid};
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct Credentials {
#[serde(skip)]
pub(crate) path: PathBuf,
pub(crate) email: String,
pub(crate) token: String,
pub(crate) expires_at: DateTime<Utc>,
}
impl Credentials {
async fn fetch_info(token: &str) -> eyre::Result<auth::info::Response> {
let url = "/auth/info";
let result = reqwest::Client::new()
.get(api_url(url))
.header("Authorization", token)
.send()
.await
.inspect_err(|e| log::error!("Request to /auth/info failed: {e:?}"))?;
let status = result.status();
let text = result.text().await?;
log::debug!("Got status from {url}: {status}");
log::debug!("Got response from {url}: {text}");
if status != StatusCode::OK {
log::error!("Auth info request status is not OK");
return Err(eyre::eyre!("Status is not 200"));
}
Ok(serde_json::from_str(&text)
.inspect_err(|e| log::error!("Could not parse auth info response: {e:?}"))?)
}
pub async fn new() -> eyre::Result<Self> {
let config = build_config()?;
let path = Path::new(config.credentials_path);
if let Ok(credentials) = Self::from_env().await.inspect_err(|error| {
log::info!("Failed to read credentials from env, skipping: {error}")
}) {
log::info!("Using credentials from env var");
return Ok(credentials);
}
if let Ok(credentials) = Self::from_keyring().inspect_err(|error| {
log::info!("Failed to get credentials from keyring, , skipping: {error}")
}) {
log::info!("Using credentials from keyring");
return Ok(credentials);
};
log::info!("Using credentials from {}", path.to_string_lossy());
let mut credentials = Self::from_file()?;
credentials.path = path.to_path_buf();
Ok(credentials)
}
pub fn is_valid(&self) -> bool {
!self.token.is_empty() && self.expires_at.timestamp() > Utc::now().timestamp()
}
pub fn write(&mut self, credentials: Credentials) -> eyre::Result<()> {
self.email = credentials.email;
self.token = credentials.token;
self.expires_at = credentials.expires_at;
if self
.save_to_keyring()
.inspect_err(|error| log::info!("keyring write error {error}"))
.is_ok()
{
return Ok(());
};
log::info!("Write token to file");
std::fs::write(self.path.clone(), json!(self).to_string()).wrap_err(Error::new(
"Failed to store credentials",
Some("File system issue, check the file permissions in ~/.kinetics/.credentials"),
))?;
Ok(())
}
pub fn delete(&self) -> eyre::Result<()> {
log::info!("Delete token from secure store");
match Self::keyring_entry()?.delete_credential() {
Ok(_) | Err(keyring::Error::NoEntry) => Ok(()),
error => error,
}?;
log::info!("Delete token from file");
if self.path.exists() {
std::fs::remove_file(&self.path).wrap_err("Failed to delete credentials file")?;
}
Ok(())
}
fn keyring_entry() -> eyre::Result<Entry> {
let user = get_user_by_uid(get_current_uid()).ok_or_eyre("Failed getting current user")?;
let username = user.name().to_str().ok_or_eyre("Invalid username")?;
log::info!("Get token entry from secure store for user {username}");
let entry = Entry::new("kinetics:api-token", username)?;
Ok(entry)
}
fn save_to_keyring(self: &Credentials) -> eyre::Result<()> {
let entry = Self::keyring_entry()?;
log::info!("Write token to secure store");
entry.set_password(&json!(self).to_string())?;
Ok(())
}
fn from_keyring() -> eyre::Result<Credentials> {
let entry = &Self::keyring_entry()?;
log::info!("Get token from secure store");
let credentials: Credentials = serde_json::from_str(&entry.get_password()?)?;
Ok(credentials)
}
async fn from_env() -> eyre::Result<Credentials> {
let config = build_config()?;
log::info!("Using credentials from env {}", config.credentials_env);
let path = Path::new(config.credentials_path);
let token = std::env::var(config.credentials_env).wrap_err(Error::new(
"Could not parse credentials file",
Some(&format!("Delete {} and try again", path.display())),
))?;
let info = Self::fetch_info(&token).await.wrap_err(Error::new(
"Failed to fetch auth info",
Some(&format!(
"Check if your {} is valid.",
config.credentials_env
)),
))?;
Ok(Credentials {
path: path.to_path_buf(),
email: info.email,
token,
expires_at: info.expires_at,
})
}
fn from_file() -> eyre::Result<Credentials> {
let config = build_config()?;
let path = Path::new(config.credentials_path);
serde_json::from_str::<crate::credentials::Credentials>(
&std::fs::read_to_string(path)
.or_else(|_| {
let default =
json!({ "email": "", "token": "", "expires_at": "2000-01-01T00:00:00Z" })
.to_string();
if let Some(dir) = path.parent() {
if !dir.exists() {
std::fs::create_dir_all(dir).wrap_err(format!(
"Failed to create dir \"{:?}\" to store credential file",
dir
))?;
}
};
eyre::Ok(default)
})
.unwrap_or_default(),
)
.wrap_err(Error::new(
"Could not parse credentials file",
Some(&format!("Delete {} and try again", path.display())),
))
}
}