From 4fd2bc8000fc50927b3f048b2873cab2138003dc Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 21 Mar 2024 17:33:24 +0100 Subject: [PATCH] Flatten the secrets config section --- crates/config/src/sections/secrets.rs | 114 ++++++++++++++++++-------- docs/config.schema.json | 38 +++------ 2 files changed, 91 insertions(+), 61 deletions(-) diff --git a/crates/config/src/sections/secrets.rs b/crates/config/src/sections/secrets.rs index 1e3b8e1c2..f6bdf2da9 100644 --- a/crates/config/src/sections/secrets.rs +++ b/crates/config/src/sections/secrets.rs @@ -14,7 +14,7 @@ use std::borrow::Cow; -use anyhow::Context; +use anyhow::{bail, Context}; use async_trait::async_trait; use camino::Utf8PathBuf; use mas_jose::jwk::{JsonWebKey, JsonWebKeySet}; @@ -35,31 +35,23 @@ fn example_secret() -> &'static str { "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" } -#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "snake_case")] -pub enum KeyOrFile { - Key(String), - #[schemars(with = "String")] - KeyFile(Utf8PathBuf), -} - -#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "snake_case")] -pub enum PasswordOrFile { - Password(String), - #[schemars(with = "String")] - PasswordFile(Utf8PathBuf), -} - #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] pub struct KeyConfig { kid: String, - #[serde(flatten)] - password: Option, + #[serde(skip_serializing_if = "Option::is_none")] + password: Option, - #[serde(flatten)] - key: KeyOrFile, + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(with = "Option")] + password_file: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + key: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(with = "Option")] + key_file: Option, } /// Application secrets @@ -90,17 +82,20 @@ impl SecretsConfig { pub async fn key_store(&self) -> anyhow::Result { let mut keys = Vec::with_capacity(self.keys.len()); for item in &self.keys { - let password = match &item.password { - Some(PasswordOrFile::Password(password)) => Some(Cow::Borrowed(password.as_str())), - Some(PasswordOrFile::PasswordFile(path)) => { - Some(Cow::Owned(tokio::fs::read_to_string(path).await?)) + let password = match (&item.password, &item.password_file) { + (None, None) => None, + (Some(_), Some(_)) => { + bail!("Cannot specify both `password` and `password_file`") } - None => None, + (Some(password), None) => Some(Cow::Borrowed(password)), + (None, Some(path)) => Some(Cow::Owned(tokio::fs::read_to_string(path).await?)), }; // Read the key either embedded in the config file or on disk - let key = match &item.key { - KeyOrFile::Key(key) => { + let key = match (&item.key, &item.key_file) { + (None, None) => bail!("Missing `key` or `key_file`"), + (Some(_), Some(_)) => bail!("Cannot specify both `key` and `key_file`"), + (Some(key), None) => { // If the key was embedded in the config file, assume it is formatted as PEM if let Some(password) = password { PrivateKey::load_encrypted_pem(key, password.as_bytes())? @@ -108,7 +103,7 @@ impl SecretsConfig { PrivateKey::load_pem(key)? } } - KeyOrFile::KeyFile(path) => { + (None, Some(path)) => { // When reading from disk, it might be either PEM or DER. `PrivateKey::load*` // will try both. let key = tokio::fs::read(path).await?; @@ -161,7 +156,9 @@ impl ConfigurationSection for SecretsConfig { let rsa_key = KeyConfig { kid: Alphanumeric.sample_string(&mut rng, 10), password: None, - key: KeyOrFile::Key(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + password_file: None, + key: Some(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + key_file: None, }; let span = tracing::info_span!("ec_p256"); @@ -177,7 +174,9 @@ impl ConfigurationSection for SecretsConfig { let ec_p256_key = KeyConfig { kid: Alphanumeric.sample_string(&mut rng, 10), password: None, - key: KeyOrFile::Key(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + password_file: None, + key: Some(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + key_file: None, }; let span = tracing::info_span!("ec_p384"); @@ -193,7 +192,9 @@ impl ConfigurationSection for SecretsConfig { let ec_p384_key = KeyConfig { kid: Alphanumeric.sample_string(&mut rng, 10), password: None, - key: KeyOrFile::Key(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + password_file: None, + key: Some(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + key_file: None, }; let span = tracing::info_span!("ec_k256"); @@ -209,7 +210,9 @@ impl ConfigurationSection for SecretsConfig { let ec_k256_key = KeyConfig { kid: Alphanumeric.sample_string(&mut rng, 10), password: None, - key: KeyOrFile::Key(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + password_file: None, + key: Some(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + key_file: None, }; Ok(Self { @@ -218,11 +221,49 @@ impl ConfigurationSection for SecretsConfig { }) } + fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + for (index, key) in self.keys.iter().enumerate() { + let annotate = |mut error: figment::Error| { + error.metadata = figment + .find_metadata(&format!("{root}.keys", root = Self::PATH.unwrap())) + .cloned(); + error.profile = Some(figment::Profile::Default); + error.path = vec![ + Self::PATH.unwrap().to_owned(), + "keys".to_owned(), + index.to_string(), + ]; + Err(error) + }; + + if key.key.is_none() && key.key_file.is_none() { + return annotate(figment::Error::from( + "Missing `key` or `key_file`".to_owned(), + )); + } + + if key.key.is_some() && key.key_file.is_some() { + return annotate(figment::Error::from( + "Cannot specify both `key` and `key_file`".to_owned(), + )); + } + + if key.password.is_some() && key.password_file.is_some() { + return annotate(figment::Error::from( + "Cannot specify both `password` and `password_file`".to_owned(), + )); + } + } + + Ok(()) + } + fn test() -> Self { let rsa_key = KeyConfig { kid: "abcdef".to_owned(), password: None, - key: KeyOrFile::Key( + password_file: None, + key: Some( indoc::indoc! {r" -----BEGIN PRIVATE KEY----- MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN @@ -237,11 +278,13 @@ impl ConfigurationSection for SecretsConfig { "} .to_owned(), ), + key_file: None, }; let ecdsa_key = KeyConfig { kid: "ghijkl".to_owned(), password: None, - key: KeyOrFile::Key( + password_file: None, + key: Some( indoc::indoc! {r" -----BEGIN PRIVATE KEY----- MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA @@ -251,6 +294,7 @@ impl ConfigurationSection for SecretsConfig { "} .to_owned(), ), + key_file: None, }; Self { diff --git a/docs/config.schema.json b/docs/config.schema.json index 412edb98b..76de3a2c4 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -1473,38 +1473,24 @@ }, "KeyConfig": { "type": "object", - "oneOf": [ - { - "type": "object", - "required": [ - "password" - ], - "properties": { - "password": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "password_file" - ], - "properties": { - "password_file": { - "type": "string" - } - }, - "additionalProperties": false - } - ], "required": [ "kid" ], "properties": { "kid": { "type": "string" + }, + "password": { + "type": "string" + }, + "password_file": { + "type": "string" + }, + "key": { + "type": "string" + }, + "key_file": { + "type": "string" } } },