git.delta.rocks / jrsonnet / refs/commits / 47baace58fe4

difftreelog

fix post secret management refactor

Yaroslav Bolyukin2024-04-26parent: #e8c42d8.patch.diff
in: trunk

1 file changed

modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
before · cmds/fleet/src/cmds/secrets/mod.rs
1use crate::{2	better_nix_eval::Field,3	fleetdata::{FleetSecret, FleetSharedSecret, SecretData},4	host::Config,5	nix_go, nix_go_json,6};7use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::{error::ErrorKind, Parser};10use crossterm::{terminal, tty::IsTty};11use itertools::Itertools;12use owo_colors::OwoColorize;13use serde::Deserialize;14use std::{15	collections::{BTreeSet, HashSet},16	ffi::OsString,17	io::{self, stdin, Cursor, Read, Write},18	path::PathBuf,19};20use tabled::{Table, Tabled};21use tempfile::NamedTempFile;22use tokio::{fs::read_to_string, process::Command};23use tracing::{error, info, info_span, warn, Instrument};2425#[derive(Parser)]26pub enum Secret {27	/// Force load host keys for all defined hosts28	ForceKeys,29	/// Add secret, data should be provided in stdin30	AddShared {31		/// Secret name32		name: String,33		/// Secret owners34		machines: Vec<String>,35		/// Override secret if already present36		#[clap(long)]37		force: bool,38		/// Secret public part39		#[clap(long)]40		public: Option<String>,41		/// Load public part from specified file42		#[clap(long)]43		public_file: Option<PathBuf>,4445		/// Create a notification on secret expiration46		#[clap(long)]47		expires_at: Option<DateTime<Utc>>,4849		/// Secret with this name already exists, override its value while keeping the same owners.50		#[clap(long)]51		re_add: bool,52	},53	/// Add secret, data should be provided in stdin54	Add {55		/// Secret name56		name: String,57		/// Secret owners58		machine: String,59		/// Override secret if already present60		#[clap(long)]61		force: bool,62		#[clap(long)]63		public: Option<String>,64		#[clap(long)]65		public_file: Option<PathBuf>,66	},67	/// Read secret from remote host, requires sudo on said host68	Read {69		name: String,70		machine: String,71		#[clap(long)]72		plaintext: bool,73	},74	ReadPublic {75		name: String,76		machine: String,77	},78	UpdateShared {79		name: String,8081		#[clap(long)]82		machines: Option<Vec<String>>,8384		#[clap(long)]85		add_machines: Vec<String>,86		#[clap(long)]87		remove_machines: Vec<String>,8889		/// Which host should we use to decrypt90		#[clap(long)]91		prefer_identities: Vec<String>,92	},93	Regenerate {94		/// Which host should we use to decrypt, in case if reencryption is required, without95		/// regeneration96		#[clap(long)]97		prefer_identities: Vec<String>,98	},99	List {},100}101102#[tracing::instrument(skip(config, secret, field, prefer_identities))]103async fn update_owner_set(104	secret_name: &str,105	config: &Config,106	mut secret: FleetSharedSecret,107	field: Field,108	updated_set: &[String],109	prefer_identities: &[String],110) -> Result<FleetSharedSecret> {111	let original_set = secret.owners.clone();112113	let set = original_set.iter().collect::<BTreeSet<_>>();114	let expected_set = updated_set.iter().collect::<BTreeSet<_>>();115116	if set == expected_set {117		info!("no need to update owner list, it is already correct");118		return Ok(secret);119	}120121	let should_regenerate = if set.difference(&expected_set).next().is_some() {122		// TODO: Remove this warning for revokable secrets.123		warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");124		nix_go_json!(field.regenerateOnOwnerRemoved)125	} else if expected_set.difference(&set).next().is_some() {126		nix_go_json!(field.regenerateOnOwnerAdded)127	} else {128		false129	};130131	if should_regenerate {132		info!("secret is owner-dependent, will regenerate");133		let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;134		Ok(generated)135	} else {136		let identity_holder = if !prefer_identities.is_empty() {137			prefer_identities138				.iter()139				.find(|i| original_set.iter().any(|s| s == *i))140		} else {141			secret.owners.first()142		};143		let Some(identity_holder) = identity_holder else {144			bail!("no available holder found");145		};146147		if let Some(data) = secret.secret.secret {148			let host = config.host(identity_holder).await?;149			let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;150			secret.secret.secret = Some(encrypted);151		}152153		secret.owners = updated_set.to_vec();154		Ok(secret)155	}156}157158#[derive(Deserialize)]159#[serde(rename_all = "camelCase")]160enum GeneratorKind {161	Impure,162	Pure,163}164165async fn generate_pure(166	_config: &Config,167	_display_name: &str,168	_secret: Field,169	_default_generator: Field,170	_owners: &[String],171) -> Result<FleetSecret> {172	bail!("pure generators are broken for now")173}174async fn generate_impure(175	config: &Config,176	_display_name: &str,177	secret: Field,178	default_generator: Field,179	owners: &[String],180) -> Result<FleetSecret> {181	let generator = nix_go!(secret.generator);182	let on: Option<String> = nix_go_json!(default_generator.impureOn);183184	let host = if let Some(on) = &on {185		config.host(on).await?186	} else {187		config.local_host()188	};189	let on_pkgs = host.pkgs().await?;190	let call_package = nix_go!(on_pkgs.callPackage);191	let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret);192193	let mut recipients = Vec::new();194	for owner in owners {195		let key = config.key(owner).await?;196		recipients.push(key);197	}198	let encrypt = nix_go!(mk_encrypt_secret(Obj {199		recipients: { recipients },200	}));201202	let generator = nix_go!(call_package(generator)(Obj {203		encrypt,204		rustfmt_please_newline: { true },205	}));206207	let generator = generator.build().await?;208	let generator = generator209		.get("out")210		.ok_or_else(|| anyhow!("missing generateImpure out"))?;211	let generator = host.remote_derivation(generator).await?;212213	let out_parent = host.mktemp_dir().await?;214	let out = format!("{out_parent}/out");215216	let mut gen = host.cmd(generator).await?;217	gen.env("out", &out);218	if on.is_none() {219		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.220		let project_path: String = config221			.directory222			.clone()223			.into_os_string()224			.into_string()225			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;226		gen.env("FLEET_PROJECT", project_path);227	}228	gen.run().await.context("impure generator")?;229230	{231		let marker = host.read_file_text(format!("{out}/marker")).await?;232		ensure!(marker == "SUCCESS", "generation not succeeded");233	}234235	let public = host.read_file_text(format!("{out}/public")).await.ok();236	let secret = host.read_file_bin(format!("{out}/secret")).await.ok();237	if let Some(secret) = &secret {238		ensure!(239			age::Decryptor::new(Cursor::new(&secret)).is_ok(),240			"builder produced non-encrypted value as secret, this is highly insecure, and not allowed."241		);242	}243244	let created_at = host.read_file_value(format!("{out}/created_at")).await?;245	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();246247	Ok(FleetSecret {248		created_at,249		expires_at,250		public,251		secret: secret.map(SecretData),252	})253}254async fn generate(255	config: &Config,256	display_name: &str,257	secret: Field,258	owners: &[String],259) -> Result<FleetSecret> {260	let generator = nix_go!(secret.generator);261	// Can't properly check on nix module system level262	{263		let gen_ty = generator.type_of().await?;264		if gen_ty == "null" {265			bail!("secret has no generator defined, can't automatically generate it.");266		}267		if gen_ty != "lambda" {268			bail!("generator should be lambda, got {gen_ty}");269		}270	}271	let default_pkgs = &config.default_pkgs;272	let default_call_package = nix_go!(default_pkgs.callPackage);273	// Generators provide additional information in passthru, to access274	// passthru we should call generator, but information about where this generator is supposed to build275	// is located in passthru... Thus evaluating generator on host.276	//277	// Maybe it is also possible to do some magic with __functor?278	//279	// I don't want to make modules always responsible for additional secret data anyway,280	// so it should be in derivation, and not in the secret data itself.281	let default_generator = nix_go!(default_call_package(generator)(Obj {}));282283	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);284285	match kind {286		GeneratorKind::Impure => {287			generate_impure(config, display_name, secret, default_generator, owners).await288		}289		GeneratorKind::Pure => {290			generate_pure(config, display_name, secret, default_generator, owners).await291		}292	}293}294async fn generate_shared(295	config: &Config,296	display_name: &str,297	secret: Field,298	expected_owners: Vec<String>,299) -> Result<FleetSharedSecret> {300	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);301	Ok(FleetSharedSecret {302		secret: generate(config, display_name, secret, &expected_owners).await?,303		owners: expected_owners,304	})305}306307async fn parse_public(308	public: Option<String>,309	public_file: Option<PathBuf>,310) -> Result<Option<String>> {311	Ok(match (public, public_file) {312		(Some(v), None) => Some(v),313		(None, Some(v)) => Some(read_to_string(v).await?),314		(Some(_), Some(_)) => {315			bail!("only public or public_file should be set")316		}317		(None, None) => None,318	})319}320321fn parse_machines(322	initial: Vec<String>,323	machines: Option<Vec<String>>,324	mut add_machines: Vec<String>,325	mut remove_machines: Vec<String>,326) -> Result<Vec<String>> {327	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {328		bail!("no operation");329	}330331	let initial_machines = initial.clone();332	let mut target_machines = initial;333	info!("Currently encrypted for {initial_machines:?}");334335	// ensure!(machines.is_some() || !add_machines.is_empty() || )336	if let Some(machines) = machines {337		ensure!(338			add_machines.is_empty() && remove_machines.is_empty(),339			"can't combine --machines and --add-machines/--remove-machines"340		);341		let target = initial_machines.iter().collect::<HashSet<_>>();342		let source = machines.iter().collect::<HashSet<_>>();343		for removed in target.difference(&source) {344			remove_machines.push((*removed).clone());345		}346		for added in source.difference(&target) {347			add_machines.push((*added).clone());348		}349	}350351	for machine in &remove_machines {352		let mut removed = false;353		while let Some(pos) = target_machines.iter().position(|m| m == machine) {354			target_machines.swap_remove(pos);355			removed = true;356		}357		if !removed {358			warn!("secret is not enabled for {machine}");359		}360	}361	for machine in &add_machines {362		if target_machines.iter().any(|m| m == machine) {363			warn!("secret is already added to {machine}");364		} else {365			target_machines.push(machine.to_owned());366		}367	}368	if !remove_machines.is_empty() {369		// TODO: maybe force secret regeneration?370		// Not that useful without revokation.371		warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");372	}373	Ok(target_machines)374}375impl Secret {376	pub async fn run(self, config: &Config) -> Result<()> {377		match self {378			Secret::ForceKeys => {379				for host in config.list_hosts().await? {380					if config.should_skip(&host.name) {381						continue;382					}383					config.key(&host.name).await?;384				}385			}386			Secret::AddShared {387				mut machines,388				name,389				force,390				public,391				public_file,392				expires_at,393				re_add,394			} => {395				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).396397				let exists = config.has_shared(&name);398				if exists && !force && !re_add {399					bail!("secret already defined");400				}401				if re_add {402					// Fixme: use clap to limit this usage403					ensure!(!force, "--force and --readd are not compatible");404					ensure!(exists, "secret doesn't exists");405					ensure!(406						machines.is_empty(),407						"you can't use machines argument for --readd"408					);409					let shared = config.shared_secret(&name)?;410					machines = shared.owners;411				}412413				let recipients = config.recipients(machines.clone()).await?;414415				let secret = {416					let mut input = vec![];417					io::stdin().read_to_end(&mut input)?;418419					if input.is_empty() {420						None421					} else {422						Some(423							SecretData::encrypt(recipients, input)424								.ok_or_else(|| anyhow!("no recipients provided"))?,425						)426					}427				};428				let public = parse_public(public, public_file).await?;429				config.replace_shared(430					name,431					FleetSharedSecret {432						owners: machines,433						secret: FleetSecret {434							created_at: Utc::now(),435							expires_at,436							secret,437							public,438						},439					},440				);441			}442			Secret::Add {443				machine,444				name,445				force,446				public,447				public_file,448			} => {449				let recipient = config.recipient(&machine).await?;450451				let secret = {452					let mut input = vec![];453					io::stdin().read_to_end(&mut input)?;454					if input.is_empty() {455						bail!("no data provided")456					}457458					Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))459				};460461				if config.has_secret(&machine, &name) && !force {462					bail!("secret already defined");463				}464				let public = parse_public(public, public_file).await?;465466				config.insert_secret(467					&machine,468					name,469					FleetSecret {470						created_at: Utc::now(),471						expires_at: None,472						secret,473						public,474					},475				);476			}477			#[allow(clippy::await_holding_refcell_ref)]478			Secret::Read {479				name,480				machine,481				plaintext,482			} => {483				let secret = config.host_secret(&machine, &name)?;484				let Some(secret) = secret.secret else {485					bail!("no secret {name}");486				};487				let host = config.host(&machine).await?;488				let data = host.decrypt(secret).await?;489				if plaintext {490					let s = String::from_utf8(data).context("output is not utf8")?;491					print!("{s}");492				} else {493					println!("{}", z85::encode(&data));494				}495			}496			Secret::ReadPublic { name, machine } => {497				let secret = config.host_secret(&machine, &name)?;498				let Some(public) = secret.public else {499					bail!("no secret {name}");500				};501				print!("{public}");502			}503			Secret::UpdateShared {504				name,505				machines,506				add_machines,507				remove_machines,508				prefer_identities,509			} => {510				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).511512				let secret = config.shared_secret(&name)?;513				if secret.secret.secret.is_none() {514					bail!("no secret");515				}516517				let initial_machines = secret.owners.clone();518				let target_machines = parse_machines(519					initial_machines.clone(),520					machines,521					add_machines,522					remove_machines,523				)?;524525				if target_machines.is_empty() {526					info!("no machines left for secret, removing it");527					config.remove_shared(&name);528					return Ok(());529				}530531				let config_field = &config.config_unchecked_field;532				let field = nix_go!(config_field.sharedSecrets[{ name }]);533534				let updated = update_owner_set(535					&name,536					config,537					secret,538					field,539					&target_machines,540					&prefer_identities,541				)542				.await?;543				config.replace_shared(name, updated);544			}545			Secret::Regenerate { prefer_identities } => {546				info!("checking for secrets to regenerate");547				{548					let _span = info_span!("shared").entered();549					let expected_shared_set = config550						.list_configured_shared()551						.await?552						.into_iter()553						.collect::<HashSet<_>>();554					let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();555					for missing in expected_shared_set.difference(&shared_set) {556						let config_field = &config.config_unchecked_field;557						let secret = nix_go!(config_field.sharedSecrets[{ missing }]);558						let expected_owners: Option<Vec<String>> =559							nix_go_json!(secret.expectedOwners);560						let Some(expected_owners) = expected_owners else {561							// TODO: Might still need to regenerate562							continue;563						};564						info!("generating secret: {missing}");565						let shared = generate_shared(config, missing, secret, expected_owners)566							.in_current_span()567							.await?;568						config.replace_shared(missing.to_string(), shared)569					}570				}571				for host in config.list_hosts().await? {572					let _span = info_span!("host", host = host.name).entered();573					let expected_set = host574						.list_configured_secrets()575						.in_current_span()576						.await?577						.into_iter()578						.collect::<HashSet<_>>();579					let stored_set = config580						.list_secrets(&host.name)581						.into_iter()582						.collect::<HashSet<_>>();583					for missing in expected_set.difference(&stored_set) {584						info!("generating secret: {missing}");585						let secret = host.secret_field(missing).in_current_span().await?;586						let generated =587							match generate(config, missing, secret, &[host.name.clone()])588								.in_current_span()589								.await590							{591								Ok(v) => v,592								Err(e) => {593									error!("{e:?}");594									continue;595								}596							};597						config.insert_secret(&host.name, missing.to_string(), generated)598					}599				}600				let mut to_remove = Vec::new();601				for name in &config.list_shared() {602					info!("updating secret: {name}");603					let data = config.shared_secret(name)?;604					let config_field = &config.config_unchecked_field;605					let expected_owners: Vec<String> =606						nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);607					if expected_owners.is_empty() {608						warn!("secret was removed from fleet config: {name}, removing from data");609						to_remove.push(name.to_string());610						continue;611					}612613					let secret = nix_go!(config_field.sharedSecrets[{ name }]);614					config.replace_shared(615						name.to_owned(),616						update_owner_set(617							name,618							config,619							data,620							secret,621							&expected_owners,622							&prefer_identities,623						)624						.await?,625					);626				}627				for k in to_remove {628					config.remove_shared(&k);629				}630			}631			Secret::List {} => {632				let _span = info_span!("loading secrets").entered();633				let configured = config.list_configured_shared().await?;634				#[derive(Tabled)]635				struct SecretDisplay {636					#[tabled(rename = "Name")]637					name: String,638					#[tabled(rename = "Owners")]639					owners: String,640				}641				let mut table = vec![];642				for name in configured.iter().cloned() {643					let config = config.clone();644					let expected_owners = config.shared_secret_expected_owners(&name).await?;645					let data = config.shared_secret(&name)?;646					let owners = data647						.owners648						.iter()649						.map(|o| {650							if expected_owners.contains(o) {651								o.green().to_string()652							} else {653								o.red().to_string()654							}655						})656						.collect::<Vec<_>>();657					table.push(SecretDisplay {658						owners: owners.join(", "),659						name,660					})661				}662				info!("loaded\n{}", Table::new(table).to_string())663			}664		}665		Ok(())666	}667}
after · cmds/fleet/src/cmds/secrets/mod.rs
1use crate::{2	better_nix_eval::Field,3	fleetdata::{FleetSecret, FleetSharedSecret, SecretData},4	host::Config,5	nix_go, nix_go_json,6};7use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::{error::ErrorKind, Parser};10use crossterm::{terminal, tty::IsTty};11use itertools::Itertools;12use owo_colors::OwoColorize;13use serde::Deserialize;14use std::{15	collections::{BTreeSet, HashSet},16	ffi::OsString,17	io::{self, stdin, Cursor, Read, Write},18	path::PathBuf,19};20use tabled::{Table, Tabled};21use tempfile::NamedTempFile;22use tokio::{fs::read_to_string, process::Command};23use tracing::{error, info, info_span, warn, Instrument};2425#[derive(Parser)]26pub enum Secret {27	/// Force load host keys for all defined hosts28	ForceKeys,29	/// Add secret, data should be provided in stdin30	AddShared {31		/// Secret name32		name: String,33		/// Secret owners34		machines: Vec<String>,35		/// Override secret if already present36		#[clap(long)]37		force: bool,38		/// Secret public part39		#[clap(long)]40		public: Option<String>,41		/// Load public part from specified file42		#[clap(long)]43		public_file: Option<PathBuf>,4445		/// Create a notification on secret expiration46		#[clap(long)]47		expires_at: Option<DateTime<Utc>>,4849		/// Secret with this name already exists, override its value while keeping the same owners.50		#[clap(long)]51		re_add: bool,52	},53	/// Add secret, data should be provided in stdin54	Add {55		/// Secret name56		name: String,57		/// Secret owners58		machine: String,59		/// Override secret if already present60		#[clap(long)]61		force: bool,62		#[clap(long)]63		public: Option<String>,64		#[clap(long)]65		public_file: Option<PathBuf>,66	},67	/// Read secret from remote host, requires sudo on said host68	Read {69		name: String,70		machine: String,71		#[clap(long)]72		plaintext: bool,73	},74	ReadPublic {75		name: String,76		machine: String,77	},78	UpdateShared {79		name: String,8081		#[clap(long)]82		machines: Option<Vec<String>>,8384		#[clap(long)]85		add_machines: Vec<String>,86		#[clap(long)]87		remove_machines: Vec<String>,8889		/// Which host should we use to decrypt90		#[clap(long)]91		prefer_identities: Vec<String>,92	},93	Regenerate {94		/// Which host should we use to decrypt, in case if reencryption is required, without95		/// regeneration96		#[clap(long)]97		prefer_identities: Vec<String>,98	},99	List {},100}101102#[tracing::instrument(skip(config, secret, field, prefer_identities))]103async fn update_owner_set(104	secret_name: &str,105	config: &Config,106	mut secret: FleetSharedSecret,107	field: Field,108	updated_set: &[String],109	prefer_identities: &[String],110) -> Result<FleetSharedSecret> {111	let original_set = secret.owners.clone();112113	let set = original_set.iter().collect::<BTreeSet<_>>();114	let expected_set = updated_set.iter().collect::<BTreeSet<_>>();115116	if set == expected_set {117		info!("no need to update owner list, it is already correct");118		return Ok(secret);119	}120121	let should_regenerate = if set.difference(&expected_set).next().is_some() {122		// TODO: Remove this warning for revokable secrets.123		warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");124		nix_go_json!(field.regenerateOnOwnerRemoved)125	} else if expected_set.difference(&set).next().is_some() {126		nix_go_json!(field.regenerateOnOwnerAdded)127	} else {128		false129	};130131	if should_regenerate {132		info!("secret is owner-dependent, will regenerate");133		let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;134		Ok(generated)135	} else {136		let identity_holder = if !prefer_identities.is_empty() {137			prefer_identities138				.iter()139				.find(|i| original_set.iter().any(|s| s == *i))140		} else {141			secret.owners.first()142		};143		let Some(identity_holder) = identity_holder else {144			bail!("no available holder found");145		};146147		if let Some(data) = secret.secret.secret {148			let host = config.host(identity_holder).await?;149			let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;150			secret.secret.secret = Some(encrypted);151		}152153		secret.owners = updated_set.to_vec();154		Ok(secret)155	}156}157158#[derive(Deserialize)]159#[serde(rename_all = "camelCase")]160enum GeneratorKind {161	Impure,162	Pure,163}164165async fn generate_pure(166	_config: &Config,167	_display_name: &str,168	_secret: Field,169	_default_generator: Field,170	_owners: &[String],171) -> Result<FleetSecret> {172	bail!("pure generators are broken for now")173}174async fn generate_impure(175	config: &Config,176	_display_name: &str,177	secret: Field,178	default_generator: Field,179	owners: &[String],180) -> Result<FleetSecret> {181	let generator = nix_go!(secret.generator);182	let on: Option<String> = nix_go_json!(default_generator.impureOn);183184	let host = if let Some(on) = &on {185		config.host(on).await?186	} else {187		config.local_host()188	};189	let on_pkgs = host.pkgs().await?;190	let call_package = nix_go!(on_pkgs.callPackage);191	let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret);192193	let mut recipients = Vec::new();194	for owner in owners {195		let key = config.key(owner).await?;196		recipients.push(key);197	}198	let encrypt = nix_go!(mk_encrypt_secret(Obj {199		recipients: { recipients },200	}));201202	let generator = nix_go!(call_package(generator)(Obj {203		encrypt,204		// rustfmt_please_newline205	}));206207	let generator = generator.build().await?;208	let generator = generator209		.get("out")210		.ok_or_else(|| anyhow!("missing generateImpure out"))?;211	let generator = host.remote_derivation(generator).await?;212213	let out_parent = host.mktemp_dir().await?;214	let out = format!("{out_parent}/out");215216	let mut gen = host.cmd(generator).await?;217	gen.env("out", &out);218	if on.is_none() {219		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.220		let project_path: String = config221			.directory222			.clone()223			.into_os_string()224			.into_string()225			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;226		gen.env("FLEET_PROJECT", project_path);227	}228	gen.run().await.context("impure generator")?;229230	{231		let marker = host.read_file_text(format!("{out}/marker")).await?;232		ensure!(marker == "SUCCESS", "generation not succeeded");233	}234235	let public = host.read_file_text(format!("{out}/public")).await.ok();236	let secret = host.read_file_bin(format!("{out}/secret")).await.ok();237	if let Some(secret) = &secret {238		ensure!(239			age::Decryptor::new(Cursor::new(&secret)).is_ok(),240			"builder produced non-encrypted value as secret, this is highly insecure, and not allowed."241		);242	}243244	let created_at = host.read_file_value(format!("{out}/created_at")).await?;245	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();246247	Ok(FleetSecret {248		created_at,249		expires_at,250		public,251		secret: secret.map(SecretData),252	})253}254async fn generate(255	config: &Config,256	display_name: &str,257	secret: Field,258	owners: &[String],259) -> Result<FleetSecret> {260	let generator = nix_go!(secret.generator);261	// Can't properly check on nix module system level262	{263		let gen_ty = generator.type_of().await?;264		if gen_ty == "null" {265			bail!("secret has no generator defined, can't automatically generate it.");266		}267		if gen_ty != "lambda" {268			bail!("generator should be lambda, got {gen_ty}");269		}270	}271	let default_pkgs = &config.default_pkgs;272	let default_call_package = nix_go!(default_pkgs.callPackage);273	// Generators provide additional information in passthru, to access274	// passthru we should call generator, but information about where this generator is supposed to build275	// is located in passthru... Thus evaluating generator on host.276	//277	// Maybe it is also possible to do some magic with __functor?278	//279	// I don't want to make modules always responsible for additional secret data anyway,280	// so it should be in derivation, and not in the secret data itself.281	let default_generator = nix_go!(default_call_package(generator)(Obj {282		encrypt: { "exit 1" },283		// rustfmt_please_newline284	}));285286	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);287288	match kind {289		GeneratorKind::Impure => {290			generate_impure(config, display_name, secret, default_generator, owners).await291		}292		GeneratorKind::Pure => {293			generate_pure(config, display_name, secret, default_generator, owners).await294		}295	}296}297async fn generate_shared(298	config: &Config,299	display_name: &str,300	secret: Field,301	expected_owners: Vec<String>,302) -> Result<FleetSharedSecret> {303	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);304	Ok(FleetSharedSecret {305		secret: generate(config, display_name, secret, &expected_owners).await?,306		owners: expected_owners,307	})308}309310async fn parse_public(311	public: Option<String>,312	public_file: Option<PathBuf>,313) -> Result<Option<String>> {314	Ok(match (public, public_file) {315		(Some(v), None) => Some(v),316		(None, Some(v)) => Some(read_to_string(v).await?),317		(Some(_), Some(_)) => {318			bail!("only public or public_file should be set")319		}320		(None, None) => None,321	})322}323324fn parse_machines(325	initial: Vec<String>,326	machines: Option<Vec<String>>,327	mut add_machines: Vec<String>,328	mut remove_machines: Vec<String>,329) -> Result<Vec<String>> {330	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {331		bail!("no operation");332	}333334	let initial_machines = initial.clone();335	let mut target_machines = initial;336	info!("Currently encrypted for {initial_machines:?}");337338	// ensure!(machines.is_some() || !add_machines.is_empty() || )339	if let Some(machines) = machines {340		ensure!(341			add_machines.is_empty() && remove_machines.is_empty(),342			"can't combine --machines and --add-machines/--remove-machines"343		);344		let target = initial_machines.iter().collect::<HashSet<_>>();345		let source = machines.iter().collect::<HashSet<_>>();346		for removed in target.difference(&source) {347			remove_machines.push((*removed).clone());348		}349		for added in source.difference(&target) {350			add_machines.push((*added).clone());351		}352	}353354	for machine in &remove_machines {355		let mut removed = false;356		while let Some(pos) = target_machines.iter().position(|m| m == machine) {357			target_machines.swap_remove(pos);358			removed = true;359		}360		if !removed {361			warn!("secret is not enabled for {machine}");362		}363	}364	for machine in &add_machines {365		if target_machines.iter().any(|m| m == machine) {366			warn!("secret is already added to {machine}");367		} else {368			target_machines.push(machine.to_owned());369		}370	}371	if !remove_machines.is_empty() {372		// TODO: maybe force secret regeneration?373		// Not that useful without revokation.374		warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");375	}376	Ok(target_machines)377}378impl Secret {379	pub async fn run(self, config: &Config) -> Result<()> {380		match self {381			Secret::ForceKeys => {382				for host in config.list_hosts().await? {383					if config.should_skip(&host.name) {384						continue;385					}386					config.key(&host.name).await?;387				}388			}389			Secret::AddShared {390				mut machines,391				name,392				force,393				public,394				public_file,395				expires_at,396				re_add,397			} => {398				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).399400				let exists = config.has_shared(&name);401				if exists && !force && !re_add {402					bail!("secret already defined");403				}404				if re_add {405					// Fixme: use clap to limit this usage406					ensure!(!force, "--force and --readd are not compatible");407					ensure!(exists, "secret doesn't exists");408					ensure!(409						machines.is_empty(),410						"you can't use machines argument for --readd"411					);412					let shared = config.shared_secret(&name)?;413					machines = shared.owners;414				}415416				let recipients = config.recipients(machines.clone()).await?;417418				let secret = {419					let mut input = vec![];420					io::stdin().read_to_end(&mut input)?;421422					if input.is_empty() {423						None424					} else {425						Some(426							SecretData::encrypt(recipients, input)427								.ok_or_else(|| anyhow!("no recipients provided"))?,428						)429					}430				};431				let public = parse_public(public, public_file).await?;432				config.replace_shared(433					name,434					FleetSharedSecret {435						owners: machines,436						secret: FleetSecret {437							created_at: Utc::now(),438							expires_at,439							secret,440							public,441						},442					},443				);444			}445			Secret::Add {446				machine,447				name,448				force,449				public,450				public_file,451			} => {452				let recipient = config.recipient(&machine).await?;453454				let secret = {455					let mut input = vec![];456					io::stdin().read_to_end(&mut input)?;457					if input.is_empty() {458						bail!("no data provided")459					}460461					Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))462				};463464				if config.has_secret(&machine, &name) && !force {465					bail!("secret already defined");466				}467				let public = parse_public(public, public_file).await?;468469				config.insert_secret(470					&machine,471					name,472					FleetSecret {473						created_at: Utc::now(),474						expires_at: None,475						secret,476						public,477					},478				);479			}480			#[allow(clippy::await_holding_refcell_ref)]481			Secret::Read {482				name,483				machine,484				plaintext,485			} => {486				let secret = config.host_secret(&machine, &name)?;487				let Some(secret) = secret.secret else {488					bail!("no secret {name}");489				};490				let host = config.host(&machine).await?;491				let data = host.decrypt(secret).await?;492				if plaintext {493					let s = String::from_utf8(data).context("output is not utf8")?;494					print!("{s}");495				} else {496					println!("{}", z85::encode(&data));497				}498			}499			Secret::ReadPublic { name, machine } => {500				let secret = config.host_secret(&machine, &name)?;501				let Some(public) = secret.public else {502					bail!("no secret {name}");503				};504				print!("{public}");505			}506			Secret::UpdateShared {507				name,508				machines,509				add_machines,510				remove_machines,511				prefer_identities,512			} => {513				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).514515				let secret = config.shared_secret(&name)?;516				if secret.secret.secret.is_none() {517					bail!("no secret");518				}519520				let initial_machines = secret.owners.clone();521				let target_machines = parse_machines(522					initial_machines.clone(),523					machines,524					add_machines,525					remove_machines,526				)?;527528				if target_machines.is_empty() {529					info!("no machines left for secret, removing it");530					config.remove_shared(&name);531					return Ok(());532				}533534				let config_field = &config.config_unchecked_field;535				let field = nix_go!(config_field.sharedSecrets[{ name }]);536537				let updated = update_owner_set(538					&name,539					config,540					secret,541					field,542					&target_machines,543					&prefer_identities,544				)545				.await?;546				config.replace_shared(name, updated);547			}548			Secret::Regenerate { prefer_identities } => {549				info!("checking for secrets to regenerate");550				{551					let _span = info_span!("shared").entered();552					let expected_shared_set = config553						.list_configured_shared()554						.await?555						.into_iter()556						.collect::<HashSet<_>>();557					let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();558					for missing in expected_shared_set.difference(&shared_set) {559						let config_field = &config.config_unchecked_field;560						let secret = nix_go!(config_field.sharedSecrets[{ missing }]);561						let expected_owners: Option<Vec<String>> =562							nix_go_json!(secret.expectedOwners);563						let Some(expected_owners) = expected_owners else {564							// TODO: Might still need to regenerate565							continue;566						};567						info!("generating secret: {missing}");568						let shared = generate_shared(config, missing, secret, expected_owners)569							.in_current_span()570							.await?;571						config.replace_shared(missing.to_string(), shared)572					}573				}574				for host in config.list_hosts().await? {575					let _span = info_span!("host", host = host.name).entered();576					let expected_set = host577						.list_configured_secrets()578						.in_current_span()579						.await?580						.into_iter()581						.collect::<HashSet<_>>();582					let stored_set = config583						.list_secrets(&host.name)584						.into_iter()585						.collect::<HashSet<_>>();586					for missing in expected_set.difference(&stored_set) {587						info!("generating secret: {missing}");588						let secret = host.secret_field(missing).in_current_span().await?;589						let generated =590							match generate(config, missing, secret, &[host.name.clone()])591								.in_current_span()592								.await593							{594								Ok(v) => v,595								Err(e) => {596									error!("{e:?}");597									continue;598								}599							};600						config.insert_secret(&host.name, missing.to_string(), generated)601					}602				}603				let mut to_remove = Vec::new();604				for name in &config.list_shared() {605					info!("updating secret: {name}");606					let data = config.shared_secret(name)?;607					let config_field = &config.config_unchecked_field;608					let expected_owners: Vec<String> =609						nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);610					if expected_owners.is_empty() {611						warn!("secret was removed from fleet config: {name}, removing from data");612						to_remove.push(name.to_string());613						continue;614					}615616					let secret = nix_go!(config_field.sharedSecrets[{ name }]);617					config.replace_shared(618						name.to_owned(),619						update_owner_set(620							name,621							config,622							data,623							secret,624							&expected_owners,625							&prefer_identities,626						)627						.await?,628					);629				}630				for k in to_remove {631					config.remove_shared(&k);632				}633			}634			Secret::List {} => {635				let _span = info_span!("loading secrets").entered();636				let configured = config.list_configured_shared().await?;637				#[derive(Tabled)]638				struct SecretDisplay {639					#[tabled(rename = "Name")]640					name: String,641					#[tabled(rename = "Owners")]642					owners: String,643				}644				let mut table = vec![];645				for name in configured.iter().cloned() {646					let config = config.clone();647					let expected_owners = config.shared_secret_expected_owners(&name).await?;648					let data = config.shared_secret(&name)?;649					let owners = data650						.owners651						.iter()652						.map(|o| {653							if expected_owners.contains(o) {654								o.green().to_string()655							} else {656								o.red().to_string()657							}658						})659						.collect::<Vec<_>>();660					table.push(SecretDisplay {661						owners: owners.join(", "),662						name,663					})664				}665				info!("loaded\n{}", Table::new(table).to_string())666			}667		}668		Ok(())669	}670}