Skip to content

Commit a613d17

Browse files
authored
Merge pull request #189 from cipherstash/configuration-error-messages
docs: improve config error messaging
2 parents f96c5d3 + 84e3dc9 commit a613d17

File tree

4 files changed

+243
-23
lines changed

4 files changed

+243
-23
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cipherstash-proxy/src/config/tandem.rs

Lines changed: 210 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use serde::Deserialize;
1515
use std::collections::HashMap;
1616
use std::env;
1717
use std::path::PathBuf;
18+
use std::sync::LazyLock;
1819
use uuid::Uuid;
1920

2021
#[derive(Clone, Debug, Deserialize)]
@@ -148,25 +149,29 @@ impl TandemConfig {
148149
.add_source(stash_setup_source)
149150
.build()?
150151
.try_deserialize()
151-
.map_err(|err| match err {
152-
config::ConfigError::Message(ref s) => match s {
152+
.map_err(|err| {
153+
// ConfigError is not helping here
154+
// - does not carry the information in structured form
155+
// - missing parameters are returned by at least two different errors, depending the source of the error
156+
// Easier to inspect the error message.
157+
match err.to_string() {
153158
s if s.contains("UUID parsing failed") => ConfigError::InvalidDatasetId,
154159
s if s.contains("missing field") => {
155-
let mut name = extract_field_name(s).map_or("unknown".to_string(), |s| s);
156-
157-
if name == "name" {
158-
name = "database.name".to_string();
160+
let (field, key) = extract_missing_field_and_key(&s);
161+
match (field, key) {
162+
(field, None) if field == "auth" => ConfigError::MissingAuthKey,
163+
(field, None) if field == "encrypt" => ConfigError::MissingEncryptKey,
164+
(field, None) if field == "database" => ConfigError::MissingDatabaseKey,
165+
(field, None) => ConfigError::MissingField { field },
166+
(field, Some(key)) => ConfigError::MissingFieldForKey { key, field },
159167
}
160-
161-
ConfigError::MissingParameter { name }
162168
}
163169
s if s.contains("does not have variant constructor") => {
164-
let (name, value) = extract_invalid_field(s);
170+
let (name, value) = extract_invalid_field(&s);
165171
ConfigError::InvalidParameter { name, value }
166172
}
167173
_ => err.into(),
168-
},
169-
_ => err.into(),
174+
}
170175
})?;
171176

172177
Ok(config)
@@ -254,18 +259,29 @@ impl Default for PrometheusConfig {
254259
}
255260
}
256261

262+
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"`([^`]+)`").unwrap());
263+
257264
///
258-
/// Extracts a field name (if present) from a config::ConfigError::Message
265+
/// Extracts a field name (if present) from a config::ConfigError string
259266
/// This is called in `build` if a ConfigError message contains the string `missing field`
267+
/// Expected string is in the forms:
268+
/// "missing field `{field}}` for key `{key}`"
269+
/// "missing field `{field}}`"
260270
///
261-
fn extract_field_name(input: &str) -> Option<String> {
262-
let re = Regex::new(r"`(\w+)`").unwrap();
263-
re.captures(input)
264-
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
271+
fn extract_missing_field_and_key(input: &str) -> (String, Option<String>) {
272+
let default = "unknown";
273+
let values = RE
274+
.find_iter(input)
275+
.map(|m| m.as_str().trim_matches('`'))
276+
.collect::<Vec<_>>();
277+
(
278+
values.first().map_or(default.to_owned(), |s| s.to_string()),
279+
values.get(1).map(|s| s.to_string()),
280+
)
265281
}
266282

267283
///
268-
/// Extracts a field name (if present) from a config::ConfigError::Message
284+
/// Extracts a field name (if present) from a config::ConfigError string
269285
/// This is called in `build` if a ConfigError message contains the string `does not have variant constructor`
270286
///
271287
/// Error string is `enum {name} does not have variant constructor {value}`
@@ -293,10 +309,14 @@ fn extract_invalid_field(input: &str) -> (String, String) {
293309

294310
#[cfg(test)]
295311
mod tests {
296-
use crate::{config::TandemConfig, error::Error};
312+
use crate::{
313+
config::{tandem::extract_missing_field_and_key, TandemConfig},
314+
error::Error,
315+
};
297316
use cipherstash_client::config::vars::{
298317
CS_CLIENT_ACCESS_KEY, CS_CLIENT_ID, CS_CLIENT_KEY, CS_DEFAULT_KEYSET_ID, CS_WORKSPACE_ID,
299318
};
319+
use std::collections::HashMap;
300320
use uuid::Uuid;
301321

302322
const CS_PREFIX: &str = "CS_TEST";
@@ -419,4 +439,176 @@ mod tests {
419439
},
420440
);
421441
}
442+
443+
#[test]
444+
/// the env vars from stash setup should be the preferred option
445+
/// File -> extended env (generated by the config struct layout) -> stash setup env
446+
fn extract_field_and_key_name_from_config_error() {
447+
let s = "missing field `client_access_key` for key `auth`";
448+
449+
let (field, key) = extract_missing_field_and_key(s);
450+
451+
assert_eq!(field, "client_access_key".to_string());
452+
assert_eq!(key.unwrap(), "auth".to_string());
453+
454+
// Bad input - auth is extracted as field
455+
let s = "blah {client_access_key} for vtha `auth`";
456+
457+
let (field, key) = extract_missing_field_and_key(s);
458+
459+
assert_eq!(field, "auth".to_string());
460+
assert!(key.is_none());
461+
462+
// Bad input - no field can be extracted is extracted as field
463+
let s = "blah {client_access_key} for vtha auth";
464+
465+
let (field, key) = extract_missing_field_and_key(s);
466+
467+
assert_eq!(field, "unknown".to_string());
468+
assert!(key.is_none());
469+
}
470+
471+
/// Returns the default environment variables as a Vec
472+
fn default_env_vars() -> Vec<(&'static str, Option<&'static str>)> {
473+
vec![
474+
("CS_CLIENT_ID", Some("00000000-0000-0000-0000-000000000000")),
475+
("CS_CLIENT_KEY", Some("CS_CLIENT_KEY")),
476+
(
477+
"CS_DEFAULT_KEYSET_ID",
478+
Some("00000000-0000-0000-0000-000000000000"),
479+
),
480+
("CS_WORKSPACE_ID", Some("CS_WORKSPACE_ID")),
481+
("CS_CLIENT_ACCESS_KEY", Some("CS_CLIENT_ACCESS_KEY")),
482+
("CS_DATABASE__USERNAME", Some("CS_DATABASE__USERNAME")),
483+
("CS_DATABASE__PASSWORD", Some("CS_DATABASE__PASSWORD")),
484+
("CS_DATABASE__NAME", Some("CS_DATABASE__NAME")),
485+
]
486+
}
487+
488+
/// Merges the default environment variables with overrides
489+
fn merge_env_vars(
490+
overrides: Vec<(&'static str, Option<&'static str>)>,
491+
) -> Vec<(&'static str, Option<&'static str>)> {
492+
let mut env_map: HashMap<&str, Option<&str>> = default_env_vars().into_iter().collect();
493+
494+
for (key, value) in overrides {
495+
env_map.insert(key, value);
496+
}
497+
498+
env_map.into_iter().collect()
499+
}
500+
501+
#[test]
502+
fn missing_auth_config() {
503+
// Missing both workspace id and client_access_key
504+
505+
let env = merge_env_vars(vec![
506+
("CS_WORKSPACE_ID", None),
507+
("CS_CLIENT_ACCESS_KEY", None),
508+
]);
509+
510+
temp_env::with_vars(env, || {
511+
let result = TandemConfig::build("tests/config/unknown.toml");
512+
assert!(result.is_err());
513+
514+
if let Err(err) = result {
515+
assert!(err.to_string().contains("Missing [auth] configuration"));
516+
} else {
517+
unreachable!();
518+
}
519+
});
520+
521+
// Missing client_access_key
522+
let env = merge_env_vars(vec![("CS_CLIENT_ACCESS_KEY", None)]);
523+
524+
temp_env::with_vars(env, || {
525+
let result = TandemConfig::build("tests/config/unknown.toml");
526+
assert!(result.is_err());
527+
528+
if let Err(err) = result {
529+
assert!(err
530+
.to_string()
531+
.contains("Missing client_access_key from [auth] configuration."));
532+
} else {
533+
unreachable!();
534+
}
535+
});
536+
}
537+
538+
#[test]
539+
fn missing_encrypt_config() {
540+
// Missing all encrypt config
541+
let env = merge_env_vars(vec![
542+
("CS_CLIENT_ID", None),
543+
("CS_CLIENT_KEY", None),
544+
("CS_DEFAULT_KEYSET_ID", None),
545+
]);
546+
547+
temp_env::with_vars(env, || {
548+
let result = TandemConfig::build("tests/config/unknown.toml");
549+
assert!(result.is_err());
550+
551+
if let Err(err) = result {
552+
assert!(err.to_string().contains("Missing [encrypt] configuration"));
553+
} else {
554+
unreachable!();
555+
}
556+
});
557+
558+
// Missing client_id
559+
let env = merge_env_vars(vec![("CS_CLIENT_ID", None)]);
560+
561+
temp_env::with_vars(env, || {
562+
let result = TandemConfig::build("tests/config/unknown.toml");
563+
assert!(result.is_err());
564+
565+
if let Err(err) = result {
566+
assert!(err
567+
.to_string()
568+
.contains("Missing client_id from [encrypt] configuration."));
569+
} else {
570+
unreachable!();
571+
}
572+
});
573+
}
574+
575+
#[test]
576+
fn missing_database_config() {
577+
// Missing all database config
578+
579+
let env = merge_env_vars(vec![
580+
("CS_DATABASE__USERNAME", None),
581+
("CS_DATABASE__PASSWORD", None),
582+
("CS_DATABASE__NAME", None),
583+
("CS_DATABASE__HOST", None),
584+
("CS_DATABASE__PORT", None),
585+
]);
586+
587+
temp_env::with_vars(env, || {
588+
let result = TandemConfig::build("tests/config/unknown.toml");
589+
assert!(result.is_err());
590+
591+
if let Err(err) = result {
592+
assert!(err.to_string().contains("Missing [database] configuration"));
593+
} else {
594+
unreachable!();
595+
}
596+
});
597+
598+
// Missing name
599+
let env = merge_env_vars(vec![("CS_DATABASE__NAME", None)]);
600+
601+
temp_env::with_vars(env, || {
602+
let result = TandemConfig::build("tests/config/unknown.toml");
603+
assert!(result.is_err());
604+
605+
if let Err(err) = result {
606+
assert!(err
607+
.to_string()
608+
.contains("Missing name from [database] configuration."));
609+
} else {
610+
unreachable!();
611+
}
612+
});
613+
}
422614
}

packages/cipherstash-proxy/src/error.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{io, time::Duration};
66
use thiserror::Error;
77

88
const ERROR_DOC_BASE_URL: &str = "https://github.com/cipherstash/proxy/blob/main/docs/errors.md";
9+
const ERROR_DOC_CONFIG_URL: &str = "https://github.com/cipherstash/proxy#configuring-proxy";
910

1011
#[derive(Error, Debug)]
1112
pub enum Error {
@@ -119,8 +120,35 @@ pub enum ConfigError {
119120
#[error("Missing an active Encrypt configuration")]
120121
MissingActiveEncryptConfig,
121122

122-
#[error("Missing field {name} from configuration file or environment")]
123-
MissingParameter { name: String },
123+
#[error(
124+
"Missing {field} from [{key}] configuration. For help visit {}",
125+
ERROR_DOC_CONFIG_URL
126+
)]
127+
MissingFieldForKey { field: String, key: String },
128+
129+
#[error(
130+
"Missing {field} from configuration. For help visit {}",
131+
ERROR_DOC_CONFIG_URL
132+
)]
133+
MissingField { field: String },
134+
135+
#[error(
136+
"Missing [auth] configuration. Check that workspace_id and client_access_key are defined. For help visit {}",
137+
ERROR_DOC_CONFIG_URL
138+
)]
139+
MissingAuthKey,
140+
141+
#[error(
142+
"Missing [encrypt] configuration. Check that client_id, client_key, and default_keyset_id are defined. For help visit {}",
143+
ERROR_DOC_CONFIG_URL
144+
)]
145+
MissingEncryptKey,
146+
147+
#[error(
148+
"Missing [database] configuration. Check that username, password, and name are defined. For help visit {}",
149+
ERROR_DOC_CONFIG_URL
150+
)]
151+
MissingDatabaseKey,
124152

125153
#[error("Expected an Encrypt configuration table")]
126154
MissingEncryptConfigTable,

packages/cipherstash-proxy/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1919
let config = match TandemConfig::load(&args) {
2020
Ok(config) => config,
2121
Err(err) => {
22-
eprintln!("Configuration Error: {}", err);
22+
eprintln!("{}", err);
2323
std::process::exit(exitcode::CONFIG);
2424
}
2525
};

0 commit comments

Comments
 (0)