git.delta.rocks / fleet / refs/commits / 12bc0d4f3eb3

difftreelog

source

crates/fleet-base/src/fleetdata.rs20.8 KiBsourcehistory
1use std::{2	cmp::Ordering,3	collections::{4		BTreeMap, BTreeSet,5		btree_map::{self, Entry},6	},7	fmt,8	io::{self, Cursor},9	sync::RwLock,10};1112use age::Recipient;13use chrono::{DateTime, Utc};14use fleet_shared::SecretData;15use rand::{16	distr::{Alphanumeric, SampleString as _},17	rng,18};19use serde::{20	Deserialize, Serialize,21	de::{self, Error},22};23use serde_json::Value;24use tracing::info;2526#[derive(Serialize, Deserialize, Default)]27#[serde(rename_all = "camelCase")]28pub struct HostData {29	#[serde(default)]30	#[serde(skip_serializing_if = "String::is_empty")]31	pub encryption_key: String,32}3334const VERSION: &str = "0.1.0";35pub struct FleetDataVersion;36impl Serialize for FleetDataVersion {37	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>38	where39		S: serde::Serializer,40	{41		VERSION.serialize(serializer)42	}43}44impl<'de> Deserialize<'de> for FleetDataVersion {45	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>46	where47		D: serde::Deserializer<'de>,48	{49		let version = String::deserialize(deserializer)?;50		if version != VERSION {51			return Err(D::Error::custom(format!(52				"fleet.nix data version mismatch, expected {VERSION}, got {version}.\nFollow the docs for migration instruction"53			)));54		}55		Ok(Self)56	}57}5859fn generate_gc_prefix() -> String {60	let id = Alphanumeric.sample_string(&mut rng(), 8);61	format!("fleet-gc-{id}")62}6364#[derive(Serialize, Deserialize)]65#[serde(rename_all = "camelCase")]66pub struct ManagerKey {67	pub name: String,68	pub key: String,69}7071#[derive(Serialize, Deserialize)]72#[serde(rename_all = "camelCase")]73pub struct FleetData {74	pub version: FleetDataVersion,75	#[serde(default = "generate_gc_prefix")]76	pub gc_root_prefix: String,7778	#[serde(default, skip_serializing_if = "Vec::is_empty")]79	pub manager_keys: Vec<ManagerKey>,8081	#[serde(default)]82	pub hosts: RwLock<BTreeMap<String, HostData>>,8384	#[serde(default, alias = "shared_secrets")]85	pub secrets: RwLock<FleetSecrets>,8687	// extra_name => anything88	#[serde(default)]89	pub extra: RwLock<BTreeMap<String, Value>>,9091	#[serde(default)]92	#[serde(skip_serializing)]93	host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,94}95impl FleetData {96	pub fn from_str(s: &str) -> anyhow::Result<Self> {97		let mut data: Self = nixlike::parse_str(s)?;98		if !data.host_secrets.is_empty() {99			info!("migrating host secrets into shared secrets structure");100			data.secrets101				.write()102				.expect("no poisoning")103				.merge_from_hosts(std::mem::take(&mut data.host_secrets));104		}105		Ok(data)106	}107}108109/// Returns None if recipients.is_empty()110pub fn encrypt_secret_data<'r>(111	recipients: impl IntoIterator<Item = &'r Box<dyn Recipient>>,112	data: Vec<u8>,113) -> Option<SecretData> {114	let mut encrypted = vec![];115	let mut encryptor = age::Encryptor::with_recipients(recipients.into_iter().map(|v| &**v))116		.ok()?117		.wrap_output(&mut encrypted)118		.expect("in memory write");119	io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");120	encryptor.finish().expect("in memory flush");121	Some(SecretData {122		data: encrypted,123		encrypted: true,124	})125}126127#[derive(Serialize, Deserialize, Clone, Debug)]128pub struct FleetSecretPart {129	pub raw: SecretData,130}131132#[derive(Serialize, Deserialize, Clone, Debug)]133#[serde(rename_all = "camelCase")]134#[must_use]135pub struct FleetSecretData {136	pub created_at: DateTime<Utc>,137	#[serde(default, skip_serializing_if = "Option::is_none", alias = "expire_at")]138	pub expires_at: Option<DateTime<Utc>>,139140	#[serde(flatten)]141	pub parts: BTreeMap<String, FleetSecretPart>,142143	#[serde(default, skip_serializing_if = "Value::is_null")]144	pub generation_data: Value,145}146147#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]148#[repr(transparent)]149pub struct SecretOwner(String);150151impl fmt::Display for SecretOwner {152	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {153		write!(f, "host:{}", self.0)154	}155}156157impl SecretOwner {158	pub fn host(s: impl AsRef<str>) -> SecretOwner {159		SecretOwner(s.as_ref().to_owned())160	}161	pub fn as_host(&self) -> Option<&str> {162		Some(&self.0)163	}164}165166#[derive(Serialize, Deserialize, Clone, Debug)]167#[serde(rename_all = "camelCase")]168#[must_use]169pub struct FleetSecretDistribution {170	#[serde(default)]171	owners: BTreeSet<SecretOwner>,172	#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]173	owners_pending_prune: BTreeMap<SecretOwner, String>,174175	#[serde(flatten)]176	pub secret: FleetSecretData,177178	#[serde(default, skip_serializing_if = "Option::is_none")]179	pending_prune: Option<String>,180	#[serde(default, skip_serializing, alias = "managed")]181	_deprecated_managed: bool,182}183184const EMPTY_PENDING_PRUNE: &BTreeMap<SecretOwner, String> = &BTreeMap::new();185impl FleetSecretDistribution {186	pub fn new(owners: BTreeSet<SecretOwner>, secret: FleetSecretData, now: DateTime<Utc>) -> Self {187		assert!(188			!owners.is_empty(),189			"distribution should have at least one owner"190		);191		if let Some(expires_at) = &secret.expires_at {192			assert!(193				*expires_at > now,194				"secret should not be expired on creation"195			);196		}197		Self {198			owners,199			secret,200			owners_pending_prune: BTreeMap::new(),201			pending_prune: None,202			_deprecated_managed: true,203		}204	}205206	fn owners_ex(&self, including_pruned: bool) -> impl Iterator<Item = &SecretOwner> {207		let pending_prune = if including_pruned {208			&self.owners_pending_prune209		} else {210			EMPTY_PENDING_PRUNE211		};212		self.owners.iter().chain(pending_prune.keys())213	}214	pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {215		self.owners_ex(false)216	}217	pub fn owners_pending_prune(&self) -> impl Iterator<Item = &SecretOwner> {218		self.owners_pending_prune.keys()219	}220	pub fn is_pending_prune(&self) -> bool {221		self.pending_prune.is_some()222	}223224	pub fn prune(&mut self, reason: String) {225		assert!(226			self.pending_prune.is_none(),227			"it shouldn't be possible to prune the same distribution twice using public api"228		);229		self.pending_prune = Some(reason);230	}231	pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {232		// if self.owners.iter().all(|o| owners.contains(o)) && self.owners_pending_prune.is_empty() {233		// 	self.prune(format!("all owners were pruned: {reason}"));234		// 	return;235		// }236		for owner in owners {237			if self.owners.remove(owner) {238				self.owners_pending_prune239					.insert(owner.to_owned(), reason.clone());240			}241		}242		// if self.owners.is_empty() {243		// 	self.prune("no owners left".to_owned());244		// }245	}246	pub fn unprune_owner(&mut self, owner: SecretOwner) {247		if self.owners_pending_prune.remove(&owner).is_some() {248			self.owners.insert(owner);249		}250	}251}252253#[derive(Clone, Debug, Default)]254#[must_use]255pub struct FleetSecretDistributions {256	stored: Vec<FleetSecretDistribution>,257}258259fn compare_dists(260	a: &FleetSecretDistribution,261	b: &FleetSecretDistribution,262	prefer_identities: &BTreeSet<SecretOwner>,263	include_pruned_owners: bool,264) -> Ordering {265	use Ordering::*;266	if prefer_identities.is_empty() {267		let a_has = a268			.owners_ex(include_pruned_owners)269			.any(|o| prefer_identities.contains(o));270		let b_has = b271			.owners_ex(include_pruned_owners)272			.any(|o| prefer_identities.contains(o));273		match (a_has, b_has) {274			(true, false) => return Greater,275			(false, true) => return Less,276			_ => {}277		}278	}279	match (a.secret.expires_at, b.secret.expires_at) {280		(None, Some(_)) => return Greater,281		(Some(_), None) => return Less,282		(Some(a), Some(b)) => {283			// Later is better284			return a.cmp(&b);285		}286		(None, None) => {}287	}288289	// Which one is easier to access290	a.owners.len().cmp(&b.owners.len())291}292293impl FleetSecretDistributions {294	/// Drop expired distributions295	fn prune_expired(&mut self, now: DateTime<Utc>) {296		for ele in self.distributions_mut() {297			if let Some(expires_at) = ele.secret.expires_at {298				if expires_at < now {299					ele.prune(format!("expired during check at {now}"));300				}301			}302		}303	}304	/// Perform all pruning relevant to shared secrets305	/// Also see expected_owner_removed306	pub fn prune_shared(307		&mut self,308		expected_owners: &BTreeSet<SecretOwner>,309		unique: bool,310		expected_parts: &BTreeMap<String, GeneratorPart>,311		expected_generation_data: &Value,312		regenerate_on_owner_removed: bool,313		regenerate_on_owner_added: bool,314		prefer_identities: &BTreeSet<SecretOwner>,315		now: DateTime<Utc>,316	) {317		self.prune_expired(now);318		self.prune_generation_data(expected_generation_data, None);319		self.prune_missing_parts(expected_parts, None);320321		let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();322323		let mut to_add = expected_owners.difference(&current_owners);324		if to_add.next().is_some() && unique && regenerate_on_owner_added {325			for dist in self.distributions_mut() {326				dist.prune(format!(327					"owners missing, can't add new distribution, regeneration preferred"328				));329			}330			return;331		}332333		for to_remove in current_owners.difference(&expected_owners) {334			self.entry(to_remove.clone()).remove(335				regenerate_on_owner_removed,336				"owner was removed from expected owners list, regenerate_on_owner_removed is set"337					.to_string(),338			);339		}340		if unique {341			self.prune_nonunique(prefer_identities);342		}343	}344	pub fn prune_host(345		&mut self,346		owner: SecretOwner,347		expected_parts: &BTreeMap<String, GeneratorPart>,348		expected_generation_data: &Value,349		now: DateTime<Utc>,350	) {351		self.prune_expired(now);352		self.prune_generation_data(expected_generation_data, Some(&owner));353		// TODO: Owner-based pruning is warranted (e.g host no longer has secret defined)354		self.prune_missing_parts(expected_parts, Some(&owner));355	}356	/// Position of best distributions as in iterator returned by distributions()357	/// None if distributions not found358	fn best_idx(359		&self,360		prefer_identities: &BTreeSet<SecretOwner>,361		include_pruned_owners: bool,362	) -> Option<usize> {363		self.distributions()364			.enumerate()365			.max_by(|(_, a), (_, b)| {366				compare_dists(&a, &b, prefer_identities, include_pruned_owners)367			})368			.map(|(p, _)| p)369	}370	/// Secret wants to be the same on all hosts, leave only one unpruned version of it371	fn prune_nonunique(&mut self, prefer_identities: &BTreeSet<SecretOwner>) {372		if self.distributions().next().is_none() {373			return;374		}375		let best = self.best_idx(prefer_identities, false).expect("not empty");376		for (i, dist) in self.distributions_mut().enumerate() {377			if i != best {378				dist.prune(379					"secret wants to be the same on all hosts, only the best one was left"380						.to_owned(),381				);382			}383		}384	}385386	pub fn try_unprune(&mut self, owner: SecretOwner) -> Option<&FleetSecretDistribution> {387		assert!(self.get(&owner).is_none(), "secret is not pruned for host");388		if let Some(dist) = self389			.distributions_mut()390			.find(|v| v.owners_pending_prune.contains_key(&owner))391		{392			dist.unprune_owner(owner);393			Some(dist)394		} else {395			None396		}397	}398399	pub fn best_distribution_for_reencryption(400		&mut self,401		prefer_identities: &BTreeSet<SecretOwner>,402	) -> Option<&mut FleetSecretDistribution> {403		let best_idx = self.best_idx(prefer_identities, true)?;404		self.distributions_mut().nth(best_idx)405	}406407	fn prune_missing_parts(408		&mut self,409		expected_parts: &BTreeMap<String, GeneratorPart>,410		filter_owner: Option<&SecretOwner>,411	) {412		'dist: for ele in self.distributions_mut() {413			if let Some(filter_owner) = filter_owner {414				if !ele.owners.contains(filter_owner) {415					continue;416				}417				// Note: secret still can have multiple owners even if it is host-owned418				// in this case we expect that all owners using the same generator, so we can prune distribution for all of them419			}420			for (name, part) in expected_parts {421				let Some(stored_part) = ele.secret.parts.get(name) else {422					ele.prune(format!("secret definition added new part: {name}"));423					continue 'dist;424				};425				if part.encrypted != stored_part.raw.encrypted {426					ele.prune(format!(427						"secret definition now requires part to be {}",428						if part.encrypted {429							"encrypted"430						} else {431							"non-encrypted"432						}433					));434					continue 'dist;435				}436			}437		}438	}439	fn prune_generation_data(440		&mut self,441		expected_generation_data: &Value,442		filter_owner: Option<&SecretOwner>,443	) {444		for ele in self.distributions_mut() {445			if let Some(filter_owner) = filter_owner {446				if !ele.owners.contains(filter_owner) {447					continue;448				}449				// Note: secret still can have multiple owners even if it is host-owned450				// in this case we expect that all owners using the same generator, so we can prune distribution for all of them451			}452			if ele.secret.generation_data != *expected_generation_data {453				ele.prune(format!(454					"expected generation data mismatch: {expected_generation_data:?}"455				));456			}457		}458	}459460	/// Prune all distributions with no unpruned owners.461	/// For ease of reencryption where possible, it is only called on persistence, when in memory - pruned owners are kept and462	/// can decrypt their secrets.463	fn prune_dead(&mut self) {464		for ele in self.distributions_mut() {465			if ele.owners.is_empty() {466				ele.prune("no owners left".to_owned());467			}468		}469	}470471	pub fn all_distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {472		self.stored.iter()473	}474	pub fn distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {475		self.stored.iter().filter(|v| v.pending_prune.is_none())476	}477	pub fn distributions_mut(&mut self) -> impl Iterator<Item = &mut FleetSecretDistribution> {478		self.stored.iter_mut().filter(|v| v.pending_prune.is_none())479	}480	pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {481		self.distributions().flat_map(|v| v.owners.iter())482	}483	#[allow(484		clippy::len_without_is_empty,485		reason = "should not be empty for a long time"486	)]487	pub fn len(&self) -> usize {488		self.distributions().count()489	}490491	pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {492		self.distributions().find(|d| d.owners.contains(owner))493	}494	fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {495		let Some((idx, dist)) = self496			.distributions()497			.enumerate()498			.find(|(_, d)| d.owners.contains(&owner))499		else {500			return DistEntry::Vacant(VacantDistEntry {501				distributions: self,502				owners: BTreeSet::from([owner]),503			});504		};505		DistEntry::Occupied(OccupiedDistEntry {506			owners: dist.owners.clone(),507			distributions: self,508			idx,509		})510	}511	pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {512		for ele in self.distributions_mut() {513			ele.prune_owners(&dist.owners, reason.clone());514		}515		self.stored.push(dist);516	}517	pub fn contains(&self, owner: &SecretOwner) -> bool {518		self.distributions().any(|d| d.owners.contains(owner))519	}520}521522struct OccupiedDistEntry<'d> {523	distributions: &'d mut FleetSecretDistributions,524	idx: usize,525	owners: BTreeSet<SecretOwner>,526}527impl<'d> OccupiedDistEntry<'d> {528	fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {529		let dist = &mut self.distributions.stored[self.idx];530		if whole_dist {531			dist.prune(reason);532		} else {533			dist.prune_owners(&self.owners, reason);534		}535		VacantDistEntry {536			distributions: self.distributions,537			owners: self.owners,538		}539	}540	fn set(self, secret: FleetSecretData, reason: String) -> Self {541		self.remove(false, reason).set(secret)542	}543}544struct VacantDistEntry<'d> {545	distributions: &'d mut FleetSecretDistributions,546	owners: BTreeSet<SecretOwner>,547}548impl<'d> VacantDistEntry<'d> {549	fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {550		let Self {551			distributions,552			owners,553		} = self;554		let idx = distributions.stored.len();555		distributions.stored.push(FleetSecretDistribution {556			owners: owners.clone(),557			secret,558559			owners_pending_prune: BTreeMap::new(),560			pending_prune: None,561			_deprecated_managed: true,562		});563		OccupiedDistEntry {564			distributions,565			owners,566			idx,567		}568	}569}570571enum DistEntry<'d> {572	Vacant(VacantDistEntry<'d>),573	Occupied(OccupiedDistEntry<'d>),574}575impl DistEntry<'_> {576	fn remove(self, whole_dist: bool, reason: String) -> Self {577		match self {578			DistEntry::Vacant(_) => self,579			DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),580		}581	}582	fn set(self, secret: FleetSecretData, reason: String) -> Self {583		Self::Occupied(match self {584			DistEntry::Vacant(e) => e.set(secret),585			DistEntry::Occupied(e) => e.set(secret, reason),586		})587	}588}589590impl Serialize for FleetSecretDistributions {591	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>592	where593		S: serde::Serializer,594	{595		let mut v = self.clone();596		v.prune_dead();597		let mut found_hosts = BTreeSet::new();598		for ele in v.distributions() {599			if ele.pending_prune.is_some() {600				continue;601			}602			if ele.owners.is_empty() {603				panic!("consistency: secret distribution has no defined owners");604			}605			for ele in ele.owners.iter() {606				if !found_hosts.insert(ele) {607					panic!(608						"consistency: secret distribution contains duplicate entry for the same host",609					);610				}611			}612		}613		match v.stored.len() {614			0 => panic!("consistency: empty distributions"),615			1 => v.stored[0].serialize(serializer),616			_ => {617				let mut sorted = v.stored.clone();618				// Store outdated distributions last619				sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);620				sorted.serialize(serializer)621			}622		}623	}624}625impl<'de> Deserialize<'de> for FleetSecretDistributions {626	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>627	where628		D: serde::Deserializer<'de>,629	{630		#[derive(Deserialize)]631		#[serde(untagged)]632		enum Distributions {633			One(FleetSecretDistribution),634			Many(Vec<FleetSecretDistribution>),635		}636		let d = Distributions::deserialize(deserializer)?;637		let stored = match d {638			Distributions::One(d) => vec![d],639			Distributions::Many(ds) => ds,640		};641		if stored.is_empty() {642			return Err(de::Error::custom("consistency: empty distributions"));643		}644		let mut found_hosts = BTreeSet::new();645		for ele in stored.iter() {646			if ele.pending_prune.is_some() {647				continue;648			}649			if ele.owners.is_empty() {650				return Err(de::Error::custom(651					"consistency: secret distribution has no defined owners",652				));653			}654			for ele in ele.owners.iter() {655				if !found_hosts.insert(ele) {656					return Err(de::Error::custom(657						"consistency: secret distribution contains duplicate entry for the same host",658					));659				}660			}661		}662		Ok(Self { stored })663	}664}665666#[derive(Deserialize, Default)]667pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);668669impl Serialize for FleetSecrets {670	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>671	where672		S: serde::Serializer,673	{674		let data: BTreeMap<String, FleetSecretDistributions> = self675			.0676			.iter()677			.filter(|(_, v)| !v.stored.is_empty())678			.map(|(k, v)| (k.clone(), v.clone()))679			.collect();680681		data.serialize(serializer)682	}683}684685impl FleetSecrets {686	pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {687		self.0.keys()688	}689690	pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {691		self.0692			.iter()693			.filter(|(_, d)| d.contains(owner))694			.map(|(n, _)| n)695	}696697	pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {698		match self.0.entry(secret) {699			Entry::Vacant(e) => {700				e.insert(FleetSecretDistributions { stored: vec![data] });701			}702			Entry::Occupied(mut e) => {703				let dists = e.get_mut();704				dists.extend(data, "secret data was replaced".to_owned())705			}706		}707	}708	pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {709		self.0.get(secret)710	}711	pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {712		self.0.get_mut(secret)713	}714715	pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {716		self.0717			.entry(secret.to_owned())718			.or_insert(FleetSecretDistributions::default())719	}720721	pub fn contains(&self, secret: &str) -> bool {722		self.0.contains_key(secret)723	}724	pub fn remove(&mut self, secret: &str) {725		self.0.remove(secret);726	}727728	fn merge_from_hosts(729		&mut self,730		host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,731	) {732		for (host, host_secrets) in host_secrets {733			for (secret_name, mut secret_data) in host_secrets {734				secret_data.owners.insert(host.clone());735				self.set_data(secret_name, secret_data);736			}737		}738	}739740	pub fn prune_host(&mut self, host: &SecretOwner, expected_nonshared: BTreeSet<String>) {741		for (name, dists) in self.0.iter_mut() {742			if expected_nonshared.contains(name) {743				continue;744			}745			for dist in dists.distributions_mut() {746				if dist.owners.contains(host) {747					dist.prune_owners(748						&BTreeSet::from([host.to_owned()]),749						"host no longer defines this secret".to_owned(),750					);751				}752			}753		}754	}755}756757#[derive(Debug, Clone)]758pub struct Expectations {759	pub owners: BTreeSet<SecretOwner>,760	pub generation_data: serde_json::Value,761	pub parts: BTreeMap<String, GeneratorPart>,762}763#[derive(Deserialize, Debug, Clone)]764pub struct GeneratorPart {765	pub encrypted: bool,766}767768#[derive(Debug, Clone, Copy)]769pub struct RegenerationConstraints {770	pub allow_different: bool,771	pub regenerate_on_owner_added: bool,772	pub regenerate_on_owner_removed: bool,773}774impl RegenerationConstraints {775	pub fn host_personal() -> Self {776		Self {777			allow_different: false,778			regenerate_on_owner_added: true,779			regenerate_on_owner_removed: true,780		}781	}782	pub fn without_preferences(self) -> Self {783		Self {784			allow_different: self.allow_different,785			regenerate_on_owner_added: false,786			regenerate_on_owner_removed: false,787		}788	}789}