use crate::{
error::{ForemanError, ForemanResult},
fs,
};
use keyring_core::api::CredentialStore;
use serde::{Deserialize, Serialize};
use std::{
path::Path,
sync::{Arc, Once},
};
use toml_edit::{value, Document, TomlError};
pub static DEFAULT_AUTH_CONFIG: &str = include_str!("../resources/default-auth.toml");
const KEYRING_SERVICE: &str = "foreman";
fn ensure_credential_store() {
static INIT: Once = Once::new();
INIT.call_once(|| match create_platform_store() {
Ok(store) => {
keyring_core::set_default_store(store);
log::debug!("Initialized OS credential store");
}
Err(e) => {
log::debug!("OS credential store unavailable: {}", e);
}
});
}
fn require_credential_store() -> ForemanResult<()> {
ensure_credential_store();
if keyring_core::get_default_store().is_none() {
return Err(ForemanError::keyring_error(
"OS credential store is not supported on this platform. \
Use `foreman github-auth` or `foreman gitlab-auth` to store tokens in auth.toml instead.",
));
}
Ok(())
}
#[cfg(target_os = "macos")]
fn create_platform_store() -> Result<Arc<CredentialStore>, String> {
let store: Arc<CredentialStore> =
apple_native_keyring_store::keychain::Store::new().map_err(|e| e.to_string())?;
Ok(store)
}
#[cfg(windows)]
fn create_platform_store() -> Result<Arc<CredentialStore>, String> {
let store: Arc<CredentialStore> =
windows_native_keyring_store::Store::new().map_err(|e| e.to_string())?;
Ok(store)
}
#[cfg(all(not(target_os = "macos"), not(windows)))]
fn create_platform_store() -> Result<Arc<CredentialStore>, String> {
Err("OS credential store not supported on this platform".to_string())
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AuthStore {
github: Option<String>,
gitlab: Option<String>,
#[serde(default)]
secure: bool,
}
impl AuthStore {
pub fn load(path: &Path) -> ForemanResult<Self> {
if let Some(contents) = fs::try_read(path)? {
let store: AuthStore = toml::from_slice(&contents)
.map_err(|error| ForemanError::auth_parsing(path, error.to_string()))?;
Ok(store)
} else {
Ok(AuthStore::default())
}
}
pub fn github(&self) -> Option<String> {
if self.secure {
if let Some(token) = get_token_from_keyring("github") {
log::debug!("Found GitHub credentials from OS keyring");
return Some(token);
}
}
if self.github.is_some() {
log::debug!("Found GitHub credentials from auth.toml");
}
self.github.clone()
}
pub fn gitlab(&self) -> Option<String> {
if self.secure {
if let Some(token) = get_token_from_keyring("gitlab") {
log::debug!("Found GitLab credentials from OS keyring");
return Some(token);
}
}
if self.gitlab.is_some() {
log::debug!("Found GitLab credentials from auth.toml");
}
self.gitlab.clone()
}
pub fn set_github_token(auth_file: &Path, token: &str) -> ForemanResult<()> {
Self::set_token_in_file(auth_file, "github", token)
}
pub fn set_gitlab_token(auth_file: &Path, token: &str) -> ForemanResult<()> {
Self::set_token_in_file(auth_file, "gitlab", token)
}
fn set_token_in_file(auth_file: &Path, key: &str, token: &str) -> ForemanResult<()> {
let contents =
fs::try_read_to_string(auth_file)?.unwrap_or_else(|| DEFAULT_AUTH_CONFIG.to_owned());
let mut store: Document = contents
.parse()
.map_err(|err: TomlError| ForemanError::auth_parsing(auth_file, err.to_string()))?;
store[key] = value(token);
let serialized = store.to_string();
fs::write(auth_file, serialized)
}
fn set_secure_flag(auth_file: &Path, enabled: bool) -> ForemanResult<()> {
let contents =
fs::try_read_to_string(auth_file)?.unwrap_or_else(|| DEFAULT_AUTH_CONFIG.to_owned());
let mut store: Document = contents
.parse()
.map_err(|err: TomlError| ForemanError::auth_parsing(auth_file, err.to_string()))?;
store["secure"] = value(enabled);
fs::write(auth_file, store.to_string())
}
pub fn set_token_secure(auth_file: &Path, provider: &str, token: &str) -> ForemanResult<()> {
require_credential_store()?;
set_token_in_keyring(provider, token)?;
Self::set_secure_flag(auth_file, true)
}
pub fn delete_token_secure(provider: &str) -> ForemanResult<()> {
require_credential_store()?;
delete_token_from_keyring(provider)
}
pub fn delete_all_tokens_secure() -> ForemanResult<()> {
require_credential_store()?;
let mut errors = Vec::new();
if let Err(e) = delete_token_from_keyring("github") {
errors.push(format!("github: {}", e));
}
if let Err(e) = delete_token_from_keyring("gitlab") {
errors.push(format!("gitlab: {}", e));
}
if errors.is_empty() {
Ok(())
} else {
Err(ForemanError::keyring_error(errors.join("; ")))
}
}
pub fn migrate_to_keyring(auth_file: &Path) -> ForemanResult<(bool, bool)> {
require_credential_store()?;
let contents = match fs::try_read(auth_file)? {
Some(c) => c,
None => return Ok((false, false)),
};
let file_store: AuthStore = toml::from_slice(&contents)
.map_err(|error| ForemanError::auth_parsing(auth_file, error.to_string()))?;
let mut github_migrated = false;
let mut gitlab_migrated = false;
if let Some(token) = &file_store.github {
set_token_in_keyring("github", token)?;
github_migrated = true;
}
if let Some(token) = &file_store.gitlab {
set_token_in_keyring("gitlab", token)?;
gitlab_migrated = true;
}
if github_migrated || gitlab_migrated {
Self::clear_migrated_tokens(auth_file, github_migrated, gitlab_migrated)?;
Self::set_secure_flag(auth_file, true)?;
}
Ok((github_migrated, gitlab_migrated))
}
fn clear_migrated_tokens(
auth_file: &Path,
clear_github: bool,
clear_gitlab: bool,
) -> ForemanResult<()> {
let contents =
fs::try_read_to_string(auth_file)?.unwrap_or_else(|| DEFAULT_AUTH_CONFIG.to_owned());
let mut store: Document = contents
.parse()
.map_err(|err: TomlError| ForemanError::auth_parsing(auth_file, err.to_string()))?;
if clear_github {
store.remove("github");
}
if clear_gitlab {
store.remove("gitlab");
}
fs::write(auth_file, store.to_string())
}
}
fn get_token_from_keyring(key: &str) -> Option<String> {
ensure_credential_store();
match keyring_core::Entry::new(KEYRING_SERVICE, key) {
Ok(entry) => match entry.get_password() {
Ok(token) if !token.is_empty() => Some(token),
_ => None,
},
Err(_) => None,
}
}
fn set_token_in_keyring(key: &str, token: &str) -> ForemanResult<()> {
ensure_credential_store();
let entry = keyring_core::Entry::new(KEYRING_SERVICE, key)
.map_err(|e| ForemanError::keyring_error(e.to_string()))?;
entry
.set_password(token)
.map_err(|e| ForemanError::keyring_error(e.to_string()))
}
fn delete_token_from_keyring(key: &str) -> ForemanResult<()> {
ensure_credential_store();
let entry = keyring_core::Entry::new(KEYRING_SERVICE, key)
.map_err(|e| ForemanError::keyring_error(e.to_string()))?;
entry
.delete_credential()
.map_err(|e| ForemanError::keyring_error(e.to_string()))
}