git.delta.rocks / jrsonnet / refs/commits / dffcb6119923

difftreelog

source

cmds/fleet/src/cmds/secrets/mod.rs8.9 KiBsourcehistory
1use std::{2	collections::{BTreeSet, HashSet},3	io::{Read, stdin},4	path::PathBuf,5};67use anyhow::{Result, bail, ensure};8use clap::Parser;9use fleet_base::{host::Config, opts::FleetOpts};10use fleet_shared::SecretData;11use tokio::fs::read;12use tracing::{info, warn};1314#[derive(Parser)]15pub enum Secret {16	/// Force load host keys for all defined hosts17	ForceKeys,18	/// Read secret from remote host, requires sudo on one of the owning hosts19	Read {20		/// Secret name to read21		name: String,2223		/// Distribution with what machine to read24		/// If not shared between multiple - defaults to single owner25		#[clap(short = 'm', long)]26		machine: Option<String>,2728		/// Which private secret part to read29		#[clap(short = 'p', long, default_value = "secret")]30		part: Option<String>,3132		/// Which host should we use to decrypt, in case if reencryption is required, without33		/// regeneration34		#[clap(long)]35		prefer_identities: Vec<String>,36	},37	/// Prune (remove, mark for regeneration) secrets38	Prune {39		/// Secret to prune40		name: String,4142		/// Machines to prune - if specified, only the choosen machines will be pruned43		#[clap(short = 'm', long)]44		machine: Vec<String>,45	},46	/// Ensure secret is generated and not expired47	Ensure {48		/// Secret to ensure generated49		name: String,5051		/// Machines to force secret for52		#[clap(short = 'm', long)]53		machine: Vec<String>,54	},55	List {},56	Edit {57		name: String,58		#[clap(short = 'm', long)]59		machine: String,6061		#[clap(long)]62		add: bool,6364		/// Which private secret part to read65		#[clap(short = 'p', long, default_value = "secret")]66		part: String,67	},68}6970async fn parse_public(71	public: Option<String>,72	public_file: Option<PathBuf>,73) -> Result<Option<SecretData>> {74	Ok(match (public, public_file) {75		(Some(v), None) => Some(SecretData {76			data: v.into(),77			encrypted: false,78		}),79		(None, Some(v)) => Some(SecretData {80			data: read(v).await?,81			encrypted: false,82		}),83		(Some(_), Some(_)) => {84			bail!("only public or public_file should be set")85		}86		(None, None) => None,87	})88}8990async fn parse_secret() -> Result<Option<Vec<u8>>> {91	let mut input = vec![];92	stdin().read_to_end(&mut input)?;93	if input.is_empty() {94		Ok(None)95	} else {96		Ok(Some(input))97	}98}99100fn parse_machines(101	initial: BTreeSet<String>,102	machines: Option<Vec<String>>,103	mut add_machines: Vec<String>,104	mut remove_machines: Vec<String>,105) -> Result<BTreeSet<String>> {106	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {107		bail!("no operation");108	}109110	let initial_machines = initial.clone();111	let mut target_machines = initial;112	info!("Currently encrypted for {initial_machines:?}");113114	if let Some(machines) = machines {115		ensure!(116			add_machines.is_empty() && remove_machines.is_empty(),117			"can't combine --machines and --add-machines/--remove-machines"118		);119		let target = initial_machines.iter().collect::<HashSet<_>>();120		let source = machines.iter().collect::<HashSet<_>>();121		for removed in target.difference(&source) {122			remove_machines.push((*removed).clone());123		}124		for added in source.difference(&target) {125			add_machines.push((*added).clone());126		}127	}128129	for machine in &remove_machines {130		if !target_machines.remove(machine) {131			warn!("secret is not enabled for {machine}");132		}133	}134	for machine in &add_machines {135		if !target_machines.insert(machine.to_owned()) {136			warn!("secret is already added to {machine}");137		}138	}139	if !remove_machines.is_empty() {140		// TODO: maybe force secret regeneration?141		// Not that useful without revokation.142		warn!(143			"secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"144		);145	}146	Ok(target_machines)147}148impl Secret {149	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {150		match self {151			Secret::ForceKeys => {152				for host in config.list_hosts()? {153					if opts.should_skip(&host)? {154						continue;155					}156					config.host_key(&host.name).await?;157				}158			}159			Secret::Read {160				name,161				machine,162				part: part_name,163				mut prefer_identities,164			} => {165				/*166				let Some(secret) = config.shared_secret(&name) else {167					bail!("secret doesn't exists");168				};169170				let dist = if secret.len() == 1 {171					&secret[0]172				} else if let Some(machine) = machine {173					let dist = secret.get(&machine);174					let Some(dist) = dist else {175						bail!("machine {machine} has no distribution of secret {name}");176					};177					prefer_identities.push(machine);178					dist179				} else {180					bail!(181						"secret {name} has shares, but no --machine specified for specifing which do you need"182					)183				};184185				let Some(part) = dist.secret.parts.get(&part_name) else {186					bail!("no part {part_name} in secret {name}");187				};188				let data = if part.raw.encrypted {189					let identity_holder = if !prefer_identities.is_empty() {190						prefer_identities191							.iter()192							.find(|i| dist.owners.iter().any(|s| s == *i))193					} else {194						dist.owners.first()195					};196					let Some(identity_holder) = identity_holder else {197						bail!("no available holder found");198					};199					let host = config.host(identity_holder)?;200					host.decrypt(part.raw.clone()).await?201				} else {202					part.raw.data.clone()203				};204				stdout().write_all(&data)?;205				*/206				todo!()207			}208			Secret::List {} => {209				/*210				let _span = info_span!("loading secrets").entered();211				let configured = config.list_configured_shared()?;212				#[derive(Tabled)]213				struct SecretDisplay {214					#[tabled(rename = "Name")]215					name: String,216					#[tabled(rename = "Owners")]217					owners: String,218				}219				// let mut table = vec![];220				for name in configured.iter().cloned() {221					let config = config.clone();222					let data = config.shared_secret(&name).expect("exists");223					/*224										let definition = config.shared_secret_definition(&name)?;225										let expectations = definition.expectations()?;226										let owners = data227											.owners()228											.map(|o| {229												if expectations.owners.contains(o) {230													o.green().to_string()231												} else {232													o.red().to_string()233												}234											})235											.collect::<Vec<_>>();236										table.push(SecretDisplay {237											owners: owners.join(", "),238											name,239										})240					*/241				}242				// info!("loaded\n{}", Table::new(table).to_string())243				*/244				todo!()245			}246			Secret::Edit {247				name,248				machine,249				part,250				add,251			} => {252				/*let secret = config253					.host_secret(&machine, &name)254					.context("secret not found")?;255				if let Some(data) = secret.secret.parts.get(&part) {256					let host = config.host(&machine)?;257					let secret = host.decrypt(data.raw.clone()).await?;258					String::from_utf8(secret).context("secret is not utf8")?259				} else if add {260					String::new()261				} else {262					bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");263				};*/264				todo!()265			}266			Secret::Prune { name, machine } => todo!(),267			Secret::Ensure { name, machine } => todo!(),268		}269		Ok(())270	}271}272273/*274async fn edit_temp_file(275	builder: tempfile::Builder<'_, '_>,276	r: Vec<u8>,277	header: &str,278	comment: &str,279) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {280	if !stdin().is_tty() {281		// TODO: Also try to open /dev/tty directly?282		bail!("stdin is not tty, can't open editor");283	}284285	use std::fmt::Write;286	let mut file = builder.tempfile()?;287288	let mut full_header = String::new();289	let mut had = false;290	for line in header.trim_end().lines() {291		had = true;292		writeln!(&mut full_header, "{comment}{line}")?;293	}294	if had {295		writeln!(&mut full_header, "{}", comment.trim_end())?;296	}297	writeln!(298		&mut full_header,299		"{comment}Do not touch this header! It will be removed automatically"300	)?;301302	file.write_all(full_header.as_bytes())?;303	file.write_all(&r)?;304305	let abs_path = file.into_temp_path();306	let editor = std::env::var_os("VISUAL")307		.or_else(|| std::env::var_os("EDITOR"))308		.unwrap_or_else(|| "vi".into());309	let editor_args = shlex::bytes::split(editor.as_encoded_bytes())310		.ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;311	let editor_args = editor_args312		.into_iter()313		.map(|v| {314			// Only ASCII subsequences are replaced315			unsafe { OsString::from_encoded_bytes_unchecked(v) }316		})317		.collect_vec();318	let Some((editor, args)) = editor_args.split_first() else {319		bail!("EDITOR env var has no command");320	};321	let mut command = Command::new(editor);322	command.args(args);323324	let path_arg = abs_path.canonicalize()?;325326	// TODO: Save full state, using tcget/_getmode/_setmode327	let was_raw = terminal::is_raw_mode_enabled()?;328	terminal::enable_raw_mode()?;329330	let status = command.arg(path_arg).status().await;331332	if !was_raw {333		terminal::disable_raw_mode()?;334	}335336	let success = match status {337		Ok(s) => s.success(),338		Err(e) if e.kind() == io::ErrorKind::NotFound => {339			bail!("editor not found")340		}341		Err(e) => bail!("editor spawn error: {e}"),342	};343344	let mut file = std::fs::read(&abs_path).context("read editor output")?;345	let Some(v) = file.strip_prefix(full_header.as_bytes()) else {346		todo!();347	};348	todo!();349350	// Ok((success, abs_path))351}352*/