support new ENV vars to get secret values through a file or a command
We want to allow passing a secret not only directly through the environment value, but also indirectly through a file path, an open file descriptor or a command that can write it to standard out. Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
		
				
					committed by
					
						 Thomas Lamprecht
						Thomas Lamprecht
					
				
			
			
				
	
			
			
			
						parent
						
							86b8ba448c
						
					
				
				
					commit
					16a01c19dd
				
			| @ -343,13 +343,8 @@ pub(crate) unsafe fn set_test_default_master_pubkey(value: Result<Option<Vec<u8> | |||||||
| pub fn get_encryption_key_password() -> Result<Vec<u8>, Error> { | pub fn get_encryption_key_password() -> Result<Vec<u8>, Error> { | ||||||
|     // fixme: implement other input methods |     // fixme: implement other input methods | ||||||
|  |  | ||||||
|     use std::env::VarError::*; |     if let Some(password) = super::get_secret_from_env("PBS_ENCRYPTION_PASSWORD")? { | ||||||
|     match std::env::var("PBS_ENCRYPTION_PASSWORD") { |         return Ok(password.as_bytes().to_vec()); | ||||||
|         Ok(p) => return Ok(p.as_bytes().to_vec()), |  | ||||||
|         Err(NotUnicode(_)) => bail!("PBS_ENCRYPTION_PASSWORD contains bad characters"), |  | ||||||
|         Err(NotPresent) => { |  | ||||||
|             // Try another method |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // If we're on a TTY, query the user for a password |     // If we're on a TTY, query the user for a password | ||||||
|  | |||||||
| @ -1,5 +1,10 @@ | |||||||
| //! Shared tools useful for common CLI clients. | //! Shared tools useful for common CLI clients. | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|  | use std::fs::File; | ||||||
|  | use std::os::unix::io::FromRawFd; | ||||||
|  | use std::env::VarError::{NotUnicode, NotPresent}; | ||||||
|  | use std::io::{BufReader, BufRead}; | ||||||
|  | use std::process::Command; | ||||||
|  |  | ||||||
| use anyhow::{bail, format_err, Context, Error}; | use anyhow::{bail, format_err, Context, Error}; | ||||||
| use serde_json::{json, Value}; | use serde_json::{json, Value}; | ||||||
| @ -7,6 +12,7 @@ use xdg::BaseDirectories; | |||||||
|  |  | ||||||
| use proxmox::{ | use proxmox::{ | ||||||
|     api::schema::*, |     api::schema::*, | ||||||
|  |     api::cli::shellword_split, | ||||||
|     tools::fs::file_get_json, |     tools::fs::file_get_json, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @ -32,6 +38,80 @@ pub const CHUNK_SIZE_SCHEMA: Schema = IntegerSchema::new("Chunk size in KB. Must | |||||||
|     .default(4096) |     .default(4096) | ||||||
|     .schema(); |     .schema(); | ||||||
|  |  | ||||||
|  | /// Helper to read a secret through a environment variable (ENV). | ||||||
|  | /// | ||||||
|  | /// Tries the following variable names in order and returns the value | ||||||
|  | /// it will resolve for the first defined one: | ||||||
|  | /// | ||||||
|  | /// BASE_NAME => use value from ENV(BASE_NAME) directly as secret | ||||||
|  | /// BASE_NAME_FD => read the secret from the specified file descriptor | ||||||
|  | /// BASE_NAME_FILE => read the secret from the specified file name | ||||||
|  | /// BASE_NAME_CMD => read the secret from specified command first line of output on stdout | ||||||
|  | /// | ||||||
|  | /// Only return the first line of data (without CRLF). | ||||||
|  | pub fn get_secret_from_env(base_name: &str) -> Result<Option<String>, Error> { | ||||||
|  |  | ||||||
|  |     let firstline = |data: String| -> String { | ||||||
|  |         match data.lines().next() { | ||||||
|  |             Some(line) => line.to_string(), | ||||||
|  |             None => String::new(), | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let firstline_file = |file: &mut File| -> Result<String, Error> { | ||||||
|  |         let reader = BufReader::new(file); | ||||||
|  |         match reader.lines().next() { | ||||||
|  |             Some(Ok(line)) => Ok(line), | ||||||
|  |             Some(Err(err)) => Err(err.into()), | ||||||
|  |             None => Ok(String::new()), | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     match std::env::var(base_name) { | ||||||
|  |         Ok(p) => return Ok(Some(firstline(p))), | ||||||
|  |         Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", base_name)), | ||||||
|  |         Err(NotPresent) => {}, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let env_name = format!("{}_FD", base_name); | ||||||
|  |     match std::env::var(&env_name) { | ||||||
|  |         Ok(fd_str) => { | ||||||
|  |             let fd: i32 = fd_str.parse() | ||||||
|  |                 .map_err(|err| format_err!("unable to parse file descriptor in ENV({}): {}", env_name, err))?; | ||||||
|  |             let mut file = unsafe { File::from_raw_fd(fd) }; | ||||||
|  |             return Ok(Some(firstline_file(&mut file)?)); | ||||||
|  |         } | ||||||
|  |         Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", env_name)), | ||||||
|  |         Err(NotPresent) => {}, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let env_name = format!("{}_FILE", base_name); | ||||||
|  |     match std::env::var(&env_name) { | ||||||
|  |         Ok(filename) => { | ||||||
|  |             let mut file = std::fs::File::open(filename) | ||||||
|  |                 .map_err(|err| format_err!("unable to open file in ENV({}): {}", env_name, err))?; | ||||||
|  |             return Ok(Some(firstline_file(&mut file)?)); | ||||||
|  |         } | ||||||
|  |         Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", env_name)), | ||||||
|  |         Err(NotPresent) => {}, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let env_name = format!("{}_CMD", base_name); | ||||||
|  |     match std::env::var(&env_name) { | ||||||
|  |         Ok(ref command) => { | ||||||
|  |             let args = shellword_split(command)?; | ||||||
|  |             let mut command = Command::new(&args[0]); | ||||||
|  |             command.args(&args[1..]); | ||||||
|  |             let output = pbs_tools::run_command(command, None)?; | ||||||
|  |             return Ok(Some(firstline(output))); | ||||||
|  |         } | ||||||
|  |         Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", env_name)), | ||||||
|  |         Err(NotPresent) => {}, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(None) | ||||||
|  | } | ||||||
|  |  | ||||||
| pub fn get_default_repository() -> Option<String> { | pub fn get_default_repository() -> Option<String> { | ||||||
|     std::env::var("PBS_REPOSITORY").ok() |     std::env::var("PBS_REPOSITORY").ok() | ||||||
| } | } | ||||||
| @ -64,13 +144,7 @@ pub fn connect(repo: &BackupRepository) -> Result<HttpClient, Error> { | |||||||
| fn connect_do(server: &str, port: u16, auth_id: &Authid) -> Result<HttpClient, Error> { | fn connect_do(server: &str, port: u16, auth_id: &Authid) -> Result<HttpClient, Error> { | ||||||
|     let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok(); |     let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok(); | ||||||
|  |  | ||||||
|     use std::env::VarError::*; |     let password = get_secret_from_env(ENV_VAR_PBS_PASSWORD)?; | ||||||
|     let password = match std::env::var(ENV_VAR_PBS_PASSWORD) { |  | ||||||
|         Ok(p) => Some(p), |  | ||||||
|         Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", ENV_VAR_PBS_PASSWORD)), |  | ||||||
|         Err(NotPresent) => None, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     let options = HttpClientOptions::new_interactive(password, fingerprint); |     let options = HttpClientOptions::new_interactive(password, fingerprint); | ||||||
|  |  | ||||||
|     HttpClient::new(server, port, auth_id, options) |     HttpClient::new(server, port, auth_id, options) | ||||||
| @ -80,7 +154,7 @@ fn connect_do(server: &str, port: u16, auth_id: &Authid) -> Result<HttpClient, E | |||||||
| pub async fn try_get(repo: &BackupRepository, url: &str) -> Value { | pub async fn try_get(repo: &BackupRepository, url: &str) -> Value { | ||||||
|  |  | ||||||
|     let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok(); |     let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok(); | ||||||
|     let password = std::env::var(ENV_VAR_PBS_PASSWORD).ok(); |     let password = get_secret_from_env(ENV_VAR_PBS_PASSWORD).unwrap_or(None); | ||||||
|  |  | ||||||
|     // ticket cache, but no questions asked |     // ticket cache, but no questions asked | ||||||
|     let options = HttpClientOptions::new_interactive(password, fingerprint) |     let options = HttpClientOptions::new_interactive(password, fingerprint) | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user