use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Result;
use cloudflare::framework::auth::Credentials;
use serde::{Deserialize, Serialize};
use crate::login::check_update_oauth_token;
use crate::settings::{get_global_config_path, Environment, QueryEnvironment};
use crate::terminal::{emoji, styles};
const CF_API_TOKEN: &str = "CF_API_TOKEN";
const CF_API_KEY: &str = "CF_API_KEY";
const CF_EMAIL: &str = "CF_EMAIL";
static ENV_VAR_WHITELIST: [&str; 3] = [CF_API_TOKEN, CF_API_KEY, CF_EMAIL];
#[cfg(test)]
use std::io::Write;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum GlobalUser {
ApiTokenAuth {
api_token: String,
},
OAuthTokenAuth {
oauth_token: String,
refresh_token: String,
expiration_time: String,
},
GlobalKeyAuth {
email: String,
api_key: String,
},
}
impl GlobalUser {
pub fn new() -> Result<Self> {
let environment = Environment::with_whitelist(ENV_VAR_WHITELIST.to_vec());
let config_path = get_global_config_path();
let mut new_user = GlobalUser::build(environment, config_path);
if let Ok(ref mut oauth_user) = new_user {
check_update_oauth_token(oauth_user)?;
}
new_user
}
fn build<T: 'static + QueryEnvironment>(environment: T, config_path: PathBuf) -> Result<Self>
where
T: config::Source + Send + Sync,
{
if let Some(user) = Self::from_env(environment) {
user
} else {
Self::from_file(config_path)
}
}
pub fn get_expiration_time(&self) -> &String {
match self {
GlobalUser::OAuthTokenAuth {
oauth_token: _,
refresh_token: _,
expiration_time,
} => expiration_time,
_ => unreachable!(),
}
}
pub fn get_refresh_token(&self) -> &String {
match self {
GlobalUser::OAuthTokenAuth {
oauth_token: _,
refresh_token,
expiration_time: _,
} => refresh_token,
_ => unreachable!(),
}
}
pub fn set_oauth_token(&mut self, new_oauth_token: String) {
match self {
GlobalUser::OAuthTokenAuth {
ref mut oauth_token,
refresh_token: _,
expiration_time: _,
} => *oauth_token = new_oauth_token,
_ => unreachable!(),
}
}
pub fn set_refresh_token(&mut self, new_refresh_token: String) {
match self {
GlobalUser::OAuthTokenAuth {
oauth_token: _,
ref mut refresh_token,
expiration_time: _,
} => *refresh_token = new_refresh_token,
_ => unreachable!(),
}
}
pub fn set_expiration_time(&mut self, new_expiration_time: String) {
match self {
GlobalUser::OAuthTokenAuth {
oauth_token: _,
refresh_token: _,
expiration_time,
} => *expiration_time = new_expiration_time,
_ => unreachable!(),
}
}
fn from_env<T: 'static + QueryEnvironment>(environment: T) -> Option<Result<Self>>
where
T: config::Source + Send + Sync,
{
if environment.empty().unwrap_or(true) {
None
} else {
let mut s = config::Config::new();
s.merge(environment).ok();
Some(GlobalUser::from_config(s))
}
}
fn from_file(config_path: PathBuf) -> Result<Self> {
let mut s = config::Config::new();
let config_str = config_path
.to_str()
.expect("global config path should be a string");
if config_path.exists() {
log::info!(
"Config path exists. Reading from config file, {}",
config_str
);
match s.merge(config::File::with_name(config_str)) {
Ok(_) => (),
Err(_) => {
let error_info = "\nFailed to read information from configuration file.";
return Self::show_config_err_info(Some(error_info.to_string()), s);
}
}
} else {
anyhow::bail!(
"config path does not exist {}. Try running `wrangler login` or `wrangler config`",
config_str
);
}
GlobalUser::from_config(s)
}
pub fn to_file(&self, config_path: &Path) -> Result<()> {
let toml: std::string::String = toml::to_string(self)?;
fs::create_dir_all(&config_path.parent().unwrap())?;
fs::write(&config_path, toml)?;
Ok(())
}
fn from_config(config: config::Config) -> Result<Self> {
let api_token = config.get_str("api_token");
let oauth_token = config.get_str("oauth_token");
let email = config.get_str("email");
let api_key = config.get_str("api_key");
let refresh_token = config.get_str("refresh_token");
let expiration_time = config.get_str("expiration_time");
if (api_token.is_ok()
&& (oauth_token.is_ok() || refresh_token.is_ok() || expiration_time.is_ok()))
|| ((oauth_token.is_ok() || refresh_token.is_ok() || expiration_time.is_ok())
&& (email.is_ok() || api_key.is_ok()))
{
let error_info = "\nMore than one authentication method (e.g. API token and OAuth token, or OAuth token and Global API key) has been found in the configuration file. Please use only one.";
let wrangler_logout_msg = styles::highlight("`wrangler logout`");
let config_path = get_global_config_path();
let more_info = format!("{}\nIf you'd like to edit the configuration file, it can be found at {}. Consider also running {} to clean up the configuration file.", error_info, config_path.to_str().unwrap(), wrangler_logout_msg);
Self::show_config_err_info(Some(more_info), config)
} else if let Ok(api_token) = api_token {
Ok(Self::ApiTokenAuth { api_token })
} else if let (Ok(email), Ok(api_key)) = (email, api_key) {
Ok(Self::GlobalKeyAuth { email, api_key })
} else if let (Ok(oauth_token), Ok(refresh_token), Ok(expiration_time)) =
(oauth_token, refresh_token, expiration_time)
{
Ok(Self::OAuthTokenAuth {
oauth_token,
refresh_token,
expiration_time,
})
} else {
let error_info = "\nNo (valid) authentication method has been found.";
Self::show_config_err_info(Some(error_info.to_string()), config)
}
}
fn show_config_err_info(
info: Option<std::string::String>,
config: config::Config,
) -> Result<Self> {
let wrangler_login_msg = styles::highlight("`wrangler login`");
let wrangler_config_msg = styles::highlight("`wrangler config`");
let vars_msg = styles::url("https://developers.cloudflare.com/workers/tooling/wrangler/configuration/#using-environment-variables");
let additional_info = match info {
Some(text) => text,
None => "".to_string(),
};
let msg = format!(
"{} Your authentication details are improperly configured.{}\nPlease run {}, {}, or visit\n{}\nfor info on configuring with environment variables",
emoji::WARN,
additional_info,
wrangler_login_msg,
wrangler_config_msg,
vars_msg
);
log::info!("{:?}", config);
anyhow::bail!(msg)
}
}
impl From<GlobalUser> for Credentials {
fn from(user: GlobalUser) -> Credentials {
match user {
GlobalUser::ApiTokenAuth { api_token } => {
Credentials::UserAuthToken { token: api_token }
}
GlobalUser::OAuthTokenAuth {
oauth_token,
refresh_token: _,
expiration_time: _,
} => Credentials::UserAuthToken { token: oauth_token },
GlobalUser::GlobalKeyAuth { email, api_key } => Credentials::UserAuthKey {
key: api_key,
email,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::tempdir;
use crate::settings::{environment::MockEnvironment, DEFAULT_CONFIG_FILE_NAME};
#[test]
fn it_can_prioritize_token_input() {
let mut mock_env = MockEnvironment::default();
mock_env.set(CF_API_TOKEN, "foo");
mock_env.set(CF_EMAIL, "test@cloudflare.com");
mock_env.set(CF_API_KEY, "bar");
let tmp_dir = tempdir().unwrap();
let config_dir = test_config_dir(&tmp_dir, None).unwrap();
let user = GlobalUser::build(mock_env, config_dir).unwrap();
assert_eq!(
user,
GlobalUser::ApiTokenAuth {
api_token: "foo".to_string(),
}
);
}
#[test]
fn it_can_prioritize_env_vars() {
let api_token = "thisisanapitoken";
let api_key = "reallylongglobalapikey";
let email = "user@example.com";
let file_user = GlobalUser::ApiTokenAuth {
api_token: api_token.to_string(),
};
let env_user = GlobalUser::GlobalKeyAuth {
api_key: api_key.to_string(),
email: email.to_string(),
};
let mut mock_env = MockEnvironment::default();
mock_env.set(CF_EMAIL, email);
mock_env.set(CF_API_KEY, api_key);
let tmp_dir = tempdir().unwrap();
let tmp_config_path = test_config_dir(&tmp_dir, Some(file_user)).unwrap();
let new_user = GlobalUser::build(mock_env, tmp_config_path).unwrap();
assert_eq!(new_user, env_user);
}
#[test]
fn it_falls_through_to_config_with_no_env_vars() {
let mock_env = MockEnvironment::default();
let user = GlobalUser::ApiTokenAuth {
api_token: "thisisanapitoken".to_string(),
};
let tmp_dir = tempdir().unwrap();
let tmp_config_path = test_config_dir(&tmp_dir, Some(user.clone())).unwrap();
let new_user = GlobalUser::build(mock_env, tmp_config_path).unwrap();
assert_eq!(new_user, user);
}
#[test]
fn it_succeeds_with_oauth_token() {
let user = GlobalUser::OAuthTokenAuth {
oauth_token: "thisisanoauthtoken".to_string(),
refresh_token: "thisisarefreshtoken".to_string(),
expiration_time: "thisisexpirationtime".to_string(),
};
let tmp_dir = tempdir().unwrap();
let tmp_config_path = test_config_dir(&tmp_dir, Some(user.clone())).unwrap();
let file_user = GlobalUser::from_file(tmp_config_path).unwrap();
assert_eq!(file_user, user);
}
#[test]
fn it_fails_if_oauth_token_incomplete_in_file() {
let tmp_dir = tempdir().unwrap();
let config_dir = test_config_dir(&tmp_dir, None).unwrap();
let mut file = fs::OpenOptions::new()
.write(true)
.open(&config_dir.as_path())
.unwrap();
let oauth_token = "oauth_token = \"thisisanoauthtoken\"";
file.write_all(oauth_token.as_bytes()).unwrap();
let file_user = GlobalUser::from_file(config_dir);
assert!(file_user.is_err());
}
#[test]
fn it_fails_if_oauth_and_api_tokens_both_exist() {
let user = GlobalUser::ApiTokenAuth {
api_token: "thisisanapitoken".to_string(),
};
let user_extra_toml: std::string::String = toml::to_string(&GlobalUser::OAuthTokenAuth {
oauth_token: "thisisanoauthtoken".to_string(),
refresh_token: "thisisarefreshtoken".to_string(),
expiration_time: "thisisexpirationtime".to_string(),
})
.unwrap();
let tmp_dir = tempdir().unwrap();
let tmp_config_path = test_config_dir(&tmp_dir, Some(user)).unwrap();
let mut temp_file = fs::OpenOptions::new()
.append(true)
.open(&tmp_config_path)
.unwrap();
temp_file.write(user_extra_toml.as_bytes()).unwrap();
let file_user = GlobalUser::from_file(tmp_config_path);
assert!(file_user.is_err());
}
#[test]
fn it_fails_if_partial_oauth_and_api_tokens_both_exist() {
let user = GlobalUser::ApiTokenAuth {
api_token: "thisisanapitoken".to_string(),
};
let user_extra_toml = "refresh_token: \"thisisarefreshtoken\"";
let tmp_dir = tempdir().unwrap();
let tmp_config_path = test_config_dir(&tmp_dir, Some(user)).unwrap();
let mut temp_file = fs::OpenOptions::new()
.append(true)
.open(&tmp_config_path)
.unwrap();
temp_file.write(user_extra_toml.as_bytes()).unwrap();
let file_user = GlobalUser::from_file(tmp_config_path);
assert!(file_user.is_err());
}
#[test]
fn it_fails_if_oauth_token_and_complete_global_key_both_exist() {
let user = GlobalUser::GlobalKeyAuth {
email: "user@example.com".to_string(),
api_key: "reallylongglobalapikey".to_string(),
};
let user_extra_toml: std::string::String = toml::to_string(&GlobalUser::OAuthTokenAuth {
oauth_token: "thisisanoauthtoken".to_string(),
refresh_token: "thisisarefreshtoken".to_string(),
expiration_time: "thisisexpirationtime".to_string(),
})
.unwrap();
let tmp_dir = tempdir().unwrap();
let tmp_config_path = test_config_dir(&tmp_dir, Some(user)).unwrap();
let mut temp_file = fs::OpenOptions::new()
.append(true)
.open(&tmp_config_path)
.unwrap();
temp_file.write(user_extra_toml.as_bytes()).unwrap();
let file_user = GlobalUser::from_file(tmp_config_path);
assert!(file_user.is_err());
}
#[test]
fn it_fails_if_partial_oauth_token_and_complete_global_key_both_exist() {
let user = GlobalUser::GlobalKeyAuth {
email: "user@example.com".to_string(),
api_key: "reallylongglobalapikey".to_string(),
};
let user_extra_toml = "refresh_token: \"thisisarefreshtoken\"";
let tmp_dir = tempdir().unwrap();
let tmp_config_path = test_config_dir(&tmp_dir, Some(user)).unwrap();
let mut temp_file = fs::OpenOptions::new()
.append(true)
.open(&tmp_config_path)
.unwrap();
temp_file.write(user_extra_toml.as_bytes()).unwrap();
let file_user = GlobalUser::from_file(tmp_config_path);
assert!(file_user.is_err());
}
#[test]
fn it_fails_if_oauth_token_and_user_email_both_exist() {
let email_config = "email = \"user@example.com\"";
let user_extra_toml: std::string::String = toml::to_string(&GlobalUser::OAuthTokenAuth {
oauth_token: "thisisanoauthtoken".to_string(),
refresh_token: "thisisarefreshtoken".to_string(),
expiration_time: "thisisexpirationtime".to_string(),
})
.unwrap();
let tmp_dir = tempdir().unwrap();
let tmp_config_path = test_config_dir(&tmp_dir, None).unwrap();
let mut temp_file = fs::OpenOptions::new()
.append(true)
.open(&tmp_config_path)
.unwrap();
temp_file.write(email_config.as_bytes()).unwrap();
temp_file.write(user_extra_toml.as_bytes()).unwrap();
let file_user = GlobalUser::from_file(tmp_config_path);
assert!(file_user.is_err());
}
#[test]
fn it_fails_if_oauth_token_and_global_key_without_user_email_both_exist() {
let api_key = "api_key = \"reallylongglobalapikey\"";
let user_extra_toml: std::string::String = toml::to_string(&GlobalUser::OAuthTokenAuth {
oauth_token: "thisisanoauthtoken".to_string(),
refresh_token: "thisisarefreshtoken".to_string(),
expiration_time: "thisisexpirationtime".to_string(),
})
.unwrap();
let tmp_dir = tempdir().unwrap();
let tmp_config_path = test_config_dir(&tmp_dir, None).unwrap();
let mut temp_file = fs::OpenOptions::new()
.append(true)
.open(&tmp_config_path)
.unwrap();
temp_file.write(api_key.as_bytes()).unwrap();
temp_file.write(user_extra_toml.as_bytes()).unwrap();
let file_user = GlobalUser::from_file(tmp_config_path);
assert!(file_user.is_err());
}
#[test]
fn it_fails_if_global_auth_incomplete_in_file() {
let tmp_dir = tempdir().unwrap();
let config_dir = test_config_dir(&tmp_dir, None).unwrap();
let mut file = fs::OpenOptions::new()
.write(true)
.open(&config_dir.as_path())
.unwrap();
let email_config = "email = \"thisisanemail\"";
file.write_all(email_config.as_bytes()).unwrap();
let file_user = GlobalUser::from_file(config_dir);
assert!(file_user.is_err());
}
#[test]
fn it_fails_if_global_auth_incomplete_in_env() {
let mut mock_env = MockEnvironment::default();
mock_env.set(CF_API_KEY, "apikey");
let tmp_dir = tempdir().unwrap();
let config_dir = test_config_dir(&tmp_dir, None).unwrap();
let new_user = GlobalUser::build(mock_env, config_dir);
assert!(new_user.is_err());
}
#[test]
fn it_succeeds_with_no_config() {
let mut mock_env = MockEnvironment::default();
mock_env.set(CF_API_KEY, "apikey");
mock_env.set(CF_EMAIL, "email");
let dummy_path = Path::new("./definitely-does-not-exist.txt").to_path_buf();
let new_user = GlobalUser::build(mock_env, dummy_path);
assert!(new_user.is_ok());
}
#[test]
fn it_fails_with_no_authentication_method() {
let mock_env = MockEnvironment::default();
let tmp_dir = tempdir().unwrap();
let config_dir = test_config_dir(&tmp_dir, None).unwrap();
let new_user = GlobalUser::build(mock_env, config_dir);
assert!(new_user.is_err());
}
fn test_config_dir(tmp_dir: &tempfile::TempDir, user: Option<GlobalUser>) -> Result<PathBuf> {
let tmp_config_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
if let Some(user_config) = user {
user_config.to_file(&tmp_config_path)?;
} else {
File::create(&tmp_config_path)?;
}
Ok(tmp_config_path)
}
}