Skip to content
This repository has been archived by the owner on Aug 3, 2023. It is now read-only.

Add credential checking logic when auth params are provided in wrangler config #842

Merged
merged 11 commits into from
Nov 11, 2019
Merged
36 changes: 36 additions & 0 deletions src/commands/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ use std::fs::File;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;

use crate::http;
use crate::http::ErrorCodeDetail;
use crate::settings::global_user::{get_global_config_dir, GlobalUser};

use cloudflare::endpoints::user::{GetUserDetails, GetUserTokenStatus};
use cloudflare::framework::apiclient::ApiClient;
use cloudflare::framework::HttpApiClientConfig;

// set the permissions on the dir, we want to avoid that other user reads to
// file
#[cfg(not(target_os = "windows"))]
Expand All @@ -18,6 +24,9 @@ pub fn set_file_mode(file: &PathBuf) {
}

pub fn global_config(user: &GlobalUser) -> Result<(), failure::Error> {
message::info("Verifying that provided credentials are valid...");
gabbifish marked this conversation as resolved.
Show resolved Hide resolved
validate_credentials(user)?;

let toml = toml::to_string(&user)?;

let config_dir = get_global_config_dir().expect("could not find global config directory");
Expand All @@ -37,3 +46,30 @@ pub fn global_config(user: &GlobalUser) -> Result<(), failure::Error> {

Ok(())
}

// validate_credentials() checks the /user/tokens/verify endpoint (for API token)
// or /user endpoint (for global API key) to ensure provided credentials actually work.
pub fn validate_credentials(user: &GlobalUser) -> Result<(), failure::Error> {
let client = http::api_client(user, HttpApiClientConfig::default())?;

match user {
GlobalUser::TokenAuth { .. } => {
match client.request(&GetUserTokenStatus {}) {
Ok(success) => {
if success.result.status == "active" {
Ok(())
} else {
failure::bail!("Auth check failed. Your token has status \"{}\", not \"active\".")
gabbifish marked this conversation as resolved.
Show resolved Hide resolved
}
},
Err(e) => failure::bail!("Auth check failed. Please make sure your API token is correct. \n{}", http::format_error(e, ErrorCodeDetail::None))
gabbifish marked this conversation as resolved.
Show resolved Hide resolved
}
}
GlobalUser::GlobalKeyAuth { .. } => {
match client.request(&GetUserDetails {}) {
Ok(_) => Ok(()),
Err(e) => failure::bail!("Auth check failed. Please make sure your email and global API key pair are correct. \n{}", http::format_error(e, ErrorCodeDetail::None)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we link here (and above) to the docs about finding your credentials? i wouldn't link anywhere in the dashboard because we don't control those, but the docs should be pretty consistent. open to being wrong about this, not blocking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weirdly enough, the best docs are in the help center (there's not a comprehensive set of API token docs in the v4 API documentation yet...). I can add a link to this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have something in the workers wrangler quick start. if it doesn't already exist, we should update it to include api token instructions as part of this project

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the above link is the most stable atm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link above seems to have outdated documentation, I'm going to open up a workers docs ticket to fix that (as well as add a section for api tokens). I'll omit the link from Wrangler for now, but will add them when the docs are ready!

Copy link
Contributor

@ashleymichal ashleymichal Nov 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it is fine to include the link, given the location will not change, only the content, and the content must be updated before we release. one less loose thread to track.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm game to change the link to be a general https://developers.cloudflare.com/workers/quickstart/#authentication link (note: this endpoint currently doesn't exist). This link would contain information about both API tokens and API keys... what do you think? In the meantime I'll add the existing link.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah that seems like the way to go. yeah add the existing link, write an issue for us here, and an accompanying issue for docs (cc @victoriabernard92 ) and this is good to merge.

gabbifish marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
132 changes: 38 additions & 94 deletions src/commands/kv/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
use std::collections::HashSet;
use std::time::Duration;

use cloudflare::framework::auth::Credentials;
use cloudflare::framework::response::ApiFailure;
use cloudflare::framework::{Environment, HttpApiClient, HttpApiClientConfig};
use cloudflare::framework::{HttpApiClient, HttpApiClientConfig};

use http::status::StatusCode;
use percent_encoding::{percent_encode, PATH_SEGMENT_ENCODE_SET};

use crate::settings::global_user::GlobalUser;
use crate::settings::target::Target;
use crate::terminal::emoji;
use crate::terminal::message;

use crate::http;
use crate::http::ErrorCodeDetail;

pub mod bucket;
pub mod bulk;
Expand All @@ -23,6 +22,40 @@ const INTERACTIVE_RESPONSE_LEN: usize = 1;
const YES: &str = "y";
const NO: &str = "n";

// Create a special API client that has a longer timeout than usual, given that KV operations
// can be lengthy if payloads are large.
fn api_client(user: &GlobalUser) -> Result<HttpApiClient, failure::Error> {
http::api_client(
user,
HttpApiClientConfig {
// Use 5 minute timeout instead of default 30-second one.
// This is useful for bulk upload operations.
http_timeout: Duration::from_secs(5 * 60),
},
)
}

fn format_error(e: ApiFailure) -> String {
http::format_error(e, ErrorCodeDetail::WorkersKV)
}

pub fn validate_target(target: &Target) -> Result<(), failure::Error> {
let mut missing_fields = Vec::new();

if target.account_id.is_empty() {
missing_fields.push("account_id")
};

if !missing_fields.is_empty() {
failure::bail!(
"Your wrangler.toml is missing the following field(s): {:?}",
missing_fields
)
} else {
Ok(())
}
}

fn check_duplicate_namespaces(target: &Target) -> bool {
// HashSet for detecting duplicate namespace bindings
let mut binding_names: HashSet<String> = HashSet::new();
Expand Down Expand Up @@ -64,41 +97,6 @@ pub fn get_namespace_id(target: &Target, binding: &str) -> Result<String, failur
)
}

fn api_client(user: &GlobalUser) -> Result<HttpApiClient, failure::Error> {
HttpApiClient::new(
Credentials::from(user.to_owned()),
HttpApiClientConfig {
// Use 5 minute timeout instead of default 30-second one.
// This is useful for bulk upload operations.
http_timeout: Duration::from_secs(5 * 60),
},
Environment::Production,
)
}

fn format_error(e: ApiFailure) -> String {
match e {
ApiFailure::Error(status, api_errors) => {
print_status_code_context(status);
let mut complete_err = "".to_string();
for error in api_errors.errors {
let error_msg =
format!("{} Error {}: {}\n", emoji::WARN, error.code, error.message);

let suggestion = help(error.code);
if !suggestion.is_empty() {
let help_msg = format!("{} {}\n", emoji::SLEUTH, suggestion);
complete_err.push_str(&format!("{}{}", error_msg, help_msg));
} else {
complete_err.push_str(&error_msg)
}
}
complete_err
}
ApiFailure::Invalid(reqwest_err) => format!("{} Error: {}", emoji::WARN, reqwest_err),
}
}

// For interactively handling deletes (and discouraging accidental deletes).
// Input like "yes", "Yes", "no", "No" will be accepted, thanks to the whitespace-stripping
// and lowercasing logic below.
Expand All @@ -119,60 +117,6 @@ fn url_encode_key(key: &str) -> String {
percent_encode(key.as_bytes(), PATH_SEGMENT_ENCODE_SET).to_string()
}

// For handling cases where the API gateway returns errors via HTTP status codes
// (no KV error code is given).
fn print_status_code_context(status_code: StatusCode) {
match status_code {
// Folks should never hit PAYLOAD_TOO_LARGE, given that Wrangler ensures that bulk file uploads
// are max ~50 MB in size. This case is handled anyways out of an abundance of caution.
StatusCode::PAYLOAD_TOO_LARGE =>
message::warn("Returned status code 413, Payload Too Large. Please make sure your upload is less than 100MB in size"),
StatusCode::GATEWAY_TIMEOUT =>
message::warn("Returned status code 504, Gateway Timeout. Please try again in a few seconds"),
_ => (),
}
}

fn help(error_code: u16) -> &'static str {
// https://api.cloudflare.com/#workers-kv-namespace-errors
match error_code {
7003 | 7000 => {
"Your wrangler.toml is likely missing the field \"account_id\", which is required to write to Workers KV."
}
// namespace errors
10010 | 10011 | 10012 | 10013 | 10014 | 10018 => {
"Run `wrangler kv:namespace list` to see your existing namespaces with IDs"
}
10009 => "Run `wrangler kv:key list` to see your existing keys", // key errors
// TODO: link to more info
// limit errors
10022 | 10024 | 10030 => "See documentation",
// TODO: link to tool for this?
// legacy namespace errors
10021 | 10035 | 10038 => "Consider moving this namespace",
// cloudflare account errors
10017 | 10026 => "Workers KV is a paid feature, please upgrade your account (https://www.cloudflare.com/products/workers-kv/)",
_ => "",
}
}

pub fn validate_target(target: &Target) -> Result<(), failure::Error> {
let mut missing_fields = Vec::new();

if target.account_id.is_empty() {
missing_fields.push("account_id")
};

if !missing_fields.is_empty() {
failure::bail!(
"Your wrangler.toml is missing the following field(s): {:?}",
missing_fields
)
} else {
Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::commands::kv;
Expand Down
94 changes: 94 additions & 0 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ use std::time::Duration;
use crate::install;
use crate::settings::global_user::GlobalUser;

use http::status::StatusCode;

use cloudflare::framework::auth::Credentials;
use cloudflare::framework::response::ApiFailure;
use cloudflare::framework::{Environment, HttpApiClient, HttpApiClientConfig};

use crate::terminal::emoji;
use crate::terminal::message;

////---------------------------OLD API CLIENT CODE---------------------------////
// todo: remove this and replace it entirely with cloudflare-rs
fn headers(feature: Option<&str>) -> HeaderMap {
let version = if install::target::DEBUG {
"dev"
Expand Down Expand Up @@ -61,3 +72,86 @@ fn add_auth_headers<'a>(headers: &'a mut HeaderMap, user: &GlobalUser) {
}
}
}

////---------------------------NEW API CLIENT CODE---------------------------////
pub fn api_client(
user: &GlobalUser,
config: HttpApiClientConfig,
) -> Result<HttpApiClient, failure::Error> {
HttpApiClient::new(
Credentials::from(user.to_owned()),
config,
Environment::Production,
)
}

// This enum allows callers of format_error() to specify a help_*() function for adding additional
// detail to error messages, if desired.
pub enum ErrorCodeDetail {
None,
WorkersKV,
}

// Format errors from the cloudflare-rs cli for printing.
pub fn format_error(e: ApiFailure, err_type: ErrorCodeDetail) -> String {
match e {
ApiFailure::Error(status, api_errors) => {
print_status_code_context(status);
let mut complete_err = "".to_string();
for error in api_errors.errors {
let error_msg = format!("{} Code {}: {}\n", emoji::WARN, error.code, error.message);

let suggestion = match err_type {
ErrorCodeDetail::WorkersKV => kv_help(error.code),
_ => "",
};
if !suggestion.is_empty() {
let help_msg = format!("{} {}\n", emoji::SLEUTH, suggestion);
complete_err.push_str(&format!("{}{}", error_msg, help_msg));
} else {
complete_err.push_str(&error_msg)
}
}
complete_err
}
ApiFailure::Invalid(reqwest_err) => format!("{} Error: {}", emoji::WARN, reqwest_err),
}
}

// For handling cases where the API gateway returns errors via HTTP status codes
// (no API-specific, more granular error code is given).
fn print_status_code_context(status_code: StatusCode) {
match status_code {
// Folks should never hit PAYLOAD_TOO_LARGE, given that Wrangler ensures that bulk file uploads
// are max ~50 MB in size. This case is handled anyways out of an abundance of caution.
StatusCode::PAYLOAD_TOO_LARGE =>
message::warn("Returned status code 413, Payload Too Large. Please make sure your upload is less than 100MB in size"),
StatusCode::GATEWAY_TIMEOUT =>
message::warn("Returned status code 504, Gateway Timeout. Please try again in a few seconds"),
_ => (),
}
}

// kv_help() provides more detailed explanations of Workers KV API error codes.
// See https://api.cloudflare.com/#workers-kv-namespace-errors for details.
fn kv_help(error_code: u16) -> &'static str {
match error_code {
7003 | 7000 => {
"Your wrangler.toml is likely missing the field \"account_id\", which is required to write to Workers KV."
}
// namespace errors
10010 | 10011 | 10012 | 10013 | 10014 | 10018 => {
"Run `wrangler kv:namespace list` to see your existing namespaces with IDs"
}
10009 => "Run `wrangler kv:key list` to see your existing keys", // key errors
// TODO: link to more info
// limit errors
10022 | 10024 | 10030 => "See documentation",
// TODO: link to tool for this?
// legacy namespace errors
10021 | 10035 | 10038 => "Consider moving this namespace",
// cloudflare account errors
10017 | 10026 => "Workers KV is a paid feature, please upgrade your account (https://www.cloudflare.com/products/workers-kv/)",
_ => "",
}
}