git.delta.rocks / jrsonnet / refs/commits / 4b3d3e3f5e1b

difftreelog

source

crates/fleet-base/src/fleetdata.rs20.9 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}146147fn is_false(b: &bool) -> bool {148	*b == false149}150151#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]152#[repr(transparent)]153pub struct SecretOwner(String);154155impl fmt::Display for SecretOwner {156	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {157		write!(f, "host:{}", self.0)158	}159}160161impl SecretOwner {162	pub fn host(s: impl AsRef<str>) -> SecretOwner {163		SecretOwner(s.as_ref().to_owned())164	}165	pub fn as_host(&self) -> Option<&str> {166		Some(&self.0)167	}168}169170#[derive(Serialize, Deserialize, Clone, Debug)]171#[serde(rename_all = "camelCase")]172#[must_use]173pub struct FleetSecretDistribution {174	#[serde(default)]175	owners: BTreeSet<SecretOwner>,176	#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]177	owners_pending_prune: BTreeMap<SecretOwner, String>,178179	#[serde(flatten)]180	pub secret: FleetSecretData,181182	#[serde(default, skip_serializing_if = "Option::is_none")]183	pending_prune: Option<String>,184	#[serde(default, skip_serializing, alias = "managed")]185	_deprecated_managed: bool,186}187188const EMPTY_PENDING_PRUNE: &BTreeMap<SecretOwner, String> = &BTreeMap::new();189impl FleetSecretDistribution {190	pub fn new(owners: BTreeSet<SecretOwner>, secret: FleetSecretData, now: DateTime<Utc>) -> Self {191		assert!(192			!owners.is_empty(),193			"distribution should have at least one owner"194		);195		if let Some(expires_at) = &secret.expires_at {196			assert!(197				*expires_at > now,198				"secret should not be expired on creation"199			);200		}201		Self {202			owners,203			secret,204			owners_pending_prune: BTreeMap::new(),205			pending_prune: None,206			_deprecated_managed: true,207		}208	}209210	fn owners_ex(&self, including_pruned: bool) -> impl Iterator<Item = &SecretOwner> {211		let pending_prune = if including_pruned {212			&self.owners_pending_prune213		} else {214			EMPTY_PENDING_PRUNE215		};216		self.owners.iter().chain(pending_prune.keys())217	}218	pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {219		self.owners_ex(false)220	}221	pub fn owners_pending_prune(&self) -> impl Iterator<Item = &SecretOwner> {222		self.owners_pending_prune.keys()223	}224	pub fn is_pending_prune(&self) -> bool {225		self.pending_prune.is_some()226	}227228	pub fn prune(&mut self, reason: String) {229		assert!(230			self.pending_prune.is_none(),231			"it shouldn't be possible to prune the same distribution twice using public api"232		);233		self.pending_prune = Some(reason);234	}235	pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {236		// if self.owners.iter().all(|o| owners.contains(o)) && self.owners_pending_prune.is_empty() {237		// 	self.prune(format!("all owners were pruned: {reason}"));238		// 	return;239		// }240		for owner in owners {241			if self.owners.remove(owner) {242				self.owners_pending_prune243					.insert(owner.to_owned(), reason.clone());244			}245		}246		// if self.owners.is_empty() {247		// 	self.prune("no owners left".to_owned());248		// }249	}250	pub fn unprune_owner(&mut self, owner: SecretOwner) {251		if self.owners_pending_prune.remove(&owner).is_some() {252			self.owners.insert(owner);253		}254	}255}256257#[derive(Clone, Debug, Default)]258#[must_use]259pub struct FleetSecretDistributions {260	stored: Vec<FleetSecretDistribution>,261}262263fn compare_dists(264	a: &FleetSecretDistribution,265	b: &FleetSecretDistribution,266	prefer_identities: &BTreeSet<SecretOwner>,267	include_pruned_owners: bool,268) -> Ordering {269	use Ordering::*;270	if prefer_identities.is_empty() {271		let a_has = a272			.owners_ex(include_pruned_owners)273			.any(|o| prefer_identities.contains(o));274		let b_has = b275			.owners_ex(include_pruned_owners)276			.any(|o| prefer_identities.contains(o));277		match (a_has, b_has) {278			(true, false) => return Greater,279			(false, true) => return Less,280			_ => {}281		}282	}283	match (a.secret.expires_at, b.secret.expires_at) {284		(None, Some(_)) => return Greater,285		(Some(_), None) => return Less,286		(Some(a), Some(b)) => {287			// Later is better288			return a.cmp(&b);289		}290		(None, None) => {}291	}292293	// Which one is easier to access294	return a.owners.len().cmp(&b.owners.len());295}296297impl FleetSecretDistributions {298	/// Drop expired distributions299	fn prune_expired(&mut self, now: DateTime<Utc>) {300		for ele in self.distributions_mut() {301			if let Some(expires_at) = ele.secret.expires_at {302				if expires_at < now {303					ele.prune(format!("expired during check at {now}"));304				}305			}306		}307	}308	/// Perform all pruning relevant to shared secrets309	/// Also see expected_owner_removed310	pub fn prune_shared(311		&mut self,312		expected_owners: &BTreeSet<SecretOwner>,313		unique: bool,314		expected_parts: &BTreeMap<String, GeneratorPart>,315		expected_generation_data: &Value,316		regenerate_on_owner_removed: bool,317		regenerate_on_owner_added: bool,318		prefer_identities: &BTreeSet<SecretOwner>,319		now: DateTime<Utc>,320	) {321		self.prune_expired(now);322		self.prune_generation_data(expected_generation_data, None);323		self.prune_missing_parts(expected_parts, None);324325		let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();326327		let mut to_add = expected_owners.difference(&current_owners);328		if to_add.next().is_some() && unique && regenerate_on_owner_added {329			for dist in self.distributions_mut() {330				dist.prune(format!(331					"owners missing, can't add new distribution, regeneration preferred"332				));333			}334			return;335		}336337		for to_remove in current_owners.difference(&expected_owners) {338			self.entry(to_remove.clone()).remove(339				regenerate_on_owner_removed,340				"owner was removed from expected owners list, regenerate_on_owner_removed is set"341					.to_string(),342			);343		}344		if unique {345			self.prune_nonunique(prefer_identities);346		}347	}348	pub fn prune_host(349		&mut self,350		owner: SecretOwner,351		expected_parts: &BTreeMap<String, GeneratorPart>,352		expected_generation_data: &Value,353		now: DateTime<Utc>,354	) {355		self.prune_expired(now);356		self.prune_generation_data(expected_generation_data, Some(&owner));357		// TODO: Owner-based pruning is warranted (e.g host no longer has secret defined)358		self.prune_missing_parts(expected_parts, Some(&owner));359	}360	/// Position of best distributions as in iterator returned by distributions()361	/// None if distributions not found362	fn best_idx(363		&self,364		prefer_identities: &BTreeSet<SecretOwner>,365		include_pruned_owners: bool,366	) -> Option<usize> {367		self.distributions()368			.enumerate()369			.max_by(|(_, a), (_, b)| {370				compare_dists(&a, &b, prefer_identities, include_pruned_owners)371			})372			.map(|(p, _)| p)373	}374	/// Secret wants to be the same on all hosts, leave only one unpruned version of it375	fn prune_nonunique(&mut self, prefer_identities: &BTreeSet<SecretOwner>) {376		if self.distributions().next().is_none() {377			return;378		}379		let best = self.best_idx(prefer_identities, false).expect("not empty");380		for (i, dist) in self.distributions_mut().enumerate() {381			if i != best {382				dist.prune(383					"secret wants to be the same on all hosts, only the best one was left"384						.to_owned(),385				);386			}387		}388	}389390	pub fn try_unprune(&mut self, owner: SecretOwner) -> Option<&FleetSecretDistribution> {391		assert!(self.get(&owner).is_none(), "secret is not pruned for host");392		if let Some(dist) = self393			.distributions_mut()394			.find(|v| v.owners_pending_prune.contains_key(&owner))395		{396			dist.unprune_owner(owner);397			Some(dist)398		} else {399			None400		}401	}402403	pub fn best_distribution_for_reencryption(404		&mut self,405		prefer_identities: &BTreeSet<SecretOwner>,406	) -> Option<&mut FleetSecretDistribution> {407		let best_idx = self.best_idx(prefer_identities, true)?;408		self.distributions_mut().nth(best_idx)409	}410411	fn prune_missing_parts(412		&mut self,413		expected_parts: &BTreeMap<String, GeneratorPart>,414		filter_owner: Option<&SecretOwner>,415	) {416		'dist: for ele in self.distributions_mut() {417			if let Some(filter_owner) = filter_owner {418				if !ele.owners.contains(filter_owner) {419					continue;420				}421				// Note: secret still can have multiple owners even if it is host-owned422				// in this case we expect that all owners using the same generator, so we can prune distribution for all of them423			}424			for (name, part) in expected_parts {425				let Some(stored_part) = ele.secret.parts.get(name) else {426					ele.prune(format!("secret definition added new part: {name}"));427					continue 'dist;428				};429				if part.encrypted != stored_part.raw.encrypted {430					ele.prune(format!(431						"secret definition now requires part to be {}",432						if part.encrypted {433							"encrypted"434						} else {435							"non-encrypted"436						}437					));438					continue 'dist;439				}440			}441		}442	}443	fn prune_generation_data(444		&mut self,445		expected_generation_data: &Value,446		filter_owner: Option<&SecretOwner>,447	) {448		for ele in self.distributions_mut() {449			if let Some(filter_owner) = filter_owner {450				if !ele.owners.contains(filter_owner) {451					continue;452				}453				// Note: secret still can have multiple owners even if it is host-owned454				// in this case we expect that all owners using the same generator, so we can prune distribution for all of them455			}456			if ele.secret.generation_data != *expected_generation_data {457				ele.prune(format!(458					"expected generation data mismatch: {expected_generation_data:?}"459				));460			}461		}462	}463464	/// Prune all distributions with no unpruned owners.465	/// For ease of reencryption where possible, it is only called on persistence, when in memory - pruned owners are kept and466	/// can decrypt their secrets.467	fn prune_dead(&mut self) {468		for ele in self.distributions_mut() {469			if ele.owners.is_empty() {470				ele.prune("no owners left".to_owned());471			}472		}473	}474475	pub fn all_distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {476		self.stored.iter()477	}478	pub fn distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {479		self.stored.iter().filter(|v| v.pending_prune.is_none())480	}481	pub fn distributions_mut(&mut self) -> impl Iterator<Item = &mut FleetSecretDistribution> {482		self.stored.iter_mut().filter(|v| v.pending_prune.is_none())483	}484	pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {485		self.distributions().flat_map(|v| v.owners.iter())486	}487	#[allow(488		clippy::len_without_is_empty,489		reason = "should not be empty for a long time"490	)]491	pub fn len(&self) -> usize {492		self.distributions().count()493	}494495	pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {496		self.distributions().find(|d| d.owners.contains(owner))497	}498	fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {499		let Some((idx, dist)) = self500			.distributions()501			.enumerate()502			.find(|(_, d)| d.owners.contains(&owner))503		else {504			return DistEntry::Vacant(VacantDistEntry {505				distributions: self,506				owners: BTreeSet::from([owner]),507			});508		};509		DistEntry::Occupied(OccupiedDistEntry {510			owners: dist.owners.clone(),511			distributions: self,512			idx,513		})514	}515	pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {516		for ele in self.distributions_mut() {517			ele.prune_owners(&dist.owners, reason.clone());518		}519		self.stored.push(dist);520	}521	pub fn contains(&self, owner: &SecretOwner) -> bool {522		self.distributions().any(|d| d.owners.contains(owner))523	}524}525526struct OccupiedDistEntry<'d> {527	distributions: &'d mut FleetSecretDistributions,528	idx: usize,529	owners: BTreeSet<SecretOwner>,530}531impl<'d> OccupiedDistEntry<'d> {532	fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {533		let dist = &mut self.distributions.stored[self.idx];534		if whole_dist {535			dist.prune(reason);536		} else {537			dist.prune_owners(&self.owners, reason);538		}539		VacantDistEntry {540			distributions: self.distributions,541			owners: self.owners,542		}543	}544	fn set(self, secret: FleetSecretData, reason: String) -> Self {545		self.remove(false, reason).set(secret)546	}547}548struct VacantDistEntry<'d> {549	distributions: &'d mut FleetSecretDistributions,550	owners: BTreeSet<SecretOwner>,551}552impl<'d> VacantDistEntry<'d> {553	fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {554		let Self {555			distributions,556			owners,557		} = self;558		let idx = distributions.stored.len();559		distributions.stored.push(FleetSecretDistribution {560			owners: owners.clone(),561			secret,562563			owners_pending_prune: BTreeMap::new(),564			pending_prune: None,565			_deprecated_managed: true,566		});567		OccupiedDistEntry {568			distributions,569			owners,570			idx,571		}572	}573}574575enum DistEntry<'d> {576	Vacant(VacantDistEntry<'d>),577	Occupied(OccupiedDistEntry<'d>),578}579impl DistEntry<'_> {580	fn remove(self, whole_dist: bool, reason: String) -> Self {581		match self {582			DistEntry::Vacant(_) => self,583			DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),584		}585	}586	fn set(self, secret: FleetSecretData, reason: String) -> Self {587		Self::Occupied(match self {588			DistEntry::Vacant(e) => e.set(secret),589			DistEntry::Occupied(e) => e.set(secret, reason),590		})591	}592}593594impl Serialize for FleetSecretDistributions {595	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>596	where597		S: serde::Serializer,598	{599		let mut v = self.clone();600		v.prune_dead();601		let mut found_hosts = BTreeSet::new();602		for ele in v.distributions() {603			if ele.pending_prune.is_some() {604				continue;605			}606			if ele.owners.is_empty() {607				panic!("consistency: secret distribution has no defined owners");608			}609			for ele in ele.owners.iter() {610				if !found_hosts.insert(ele) {611					panic!(612						"consistency: secret distribution contains duplicate entry for the same host",613					);614				}615			}616		}617		match v.stored.len() {618			0 => panic!("consistency: empty distributions"),619			1 => v.stored[0].serialize(serializer),620			_ => {621				let mut sorted = v.stored.clone();622				// Store outdated distributions last623				sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);624				sorted.serialize(serializer)625			}626		}627	}628}629impl<'de> Deserialize<'de> for FleetSecretDistributions {630	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>631	where632		D: serde::Deserializer<'de>,633	{634		#[derive(Deserialize)]635		#[serde(untagged)]636		enum Distributions {637			One(FleetSecretDistribution),638			Many(Vec<FleetSecretDistribution>),639		}640		let d = Distributions::deserialize(deserializer)?;641		let stored = match d {642			Distributions::One(d) => vec![d],643			Distributions::Many(ds) => ds,644		};645		if stored.is_empty() {646			return Err(de::Error::custom("consistency: empty distributions"));647		}648		let mut found_hosts = BTreeSet::new();649		for ele in stored.iter() {650			if ele.pending_prune.is_some() {651				continue;652			}653			if ele.owners.is_empty() {654				return Err(de::Error::custom(655					"consistency: secret distribution has no defined owners",656				));657			}658			for ele in ele.owners.iter() {659				if !found_hosts.insert(ele) {660					return Err(de::Error::custom(661						"consistency: secret distribution contains duplicate entry for the same host",662					));663				}664			}665		}666		Ok(Self { stored })667	}668}669670#[derive(Deserialize, Default)]671pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);672673impl Serialize for FleetSecrets {674	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>675	where676		S: serde::Serializer,677	{678		let data: BTreeMap<String, FleetSecretDistributions> = self679			.0680			.iter()681			.filter(|(_, v)| !v.stored.is_empty())682			.map(|(k, v)| (k.clone(), v.clone()))683			.collect();684685		data.serialize(serializer)686	}687}688689impl FleetSecrets {690	pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {691		self.0.keys()692	}693694	pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {695		self.0696			.iter()697			.filter(|(_, d)| d.contains(owner))698			.map(|(n, _)| n)699	}700701	pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {702		match self.0.entry(secret) {703			Entry::Vacant(e) => {704				e.insert(FleetSecretDistributions { stored: vec![data] });705			}706			Entry::Occupied(mut e) => {707				let dists = e.get_mut();708				dists.extend(data, "secret data was replaced".to_owned())709			}710		}711	}712	pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {713		self.0.get(secret)714	}715	pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {716		self.0.get_mut(secret)717	}718719	pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {720		self.0721			.entry(secret.to_owned())722			.or_insert(FleetSecretDistributions::default())723	}724725	pub fn contains(&self, secret: &str) -> bool {726		self.0.contains_key(secret)727	}728	pub fn remove(&mut self, secret: &str) {729		self.0.remove(secret);730	}731732	fn merge_from_hosts(733		&mut self,734		host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,735	) {736		for (host, host_secrets) in host_secrets {737			for (secret_name, mut secret_data) in host_secrets {738				secret_data.owners.insert(host.clone());739				self.set_data(secret_name, secret_data);740			}741		}742	}743744	pub fn prune_host(&mut self, host: &SecretOwner, expected_nonshared: BTreeSet<String>) {745		for (name, dists) in self.0.iter_mut() {746			if expected_nonshared.contains(name) {747				continue;748			}749			for dist in dists.distributions_mut() {750				if dist.owners.contains(host) {751					dist.prune_owners(752						&BTreeSet::from([host.to_owned()]),753						"host no longer defines this secret".to_owned(),754					);755				}756			}757		}758	}759}760761#[derive(Debug, Clone)]762pub struct Expectations {763	pub owners: BTreeSet<SecretOwner>,764	pub generation_data: serde_json::Value,765	pub parts: BTreeMap<String, GeneratorPart>,766}767#[derive(Deserialize, Debug, Clone)]768pub struct GeneratorPart {769	pub encrypted: bool,770}771772#[derive(Debug, Clone, Copy)]773pub struct RegenerationConstraints {774	pub allow_different: bool,775	pub regenerate_on_owner_added: bool,776	pub regenerate_on_owner_removed: bool,777}778impl RegenerationConstraints {779	pub fn host_personal() -> Self {780		Self {781			allow_different: false,782			regenerate_on_owner_added: true,783			regenerate_on_owner_removed: true,784		}785	}786	pub fn without_preferences(self) -> Self {787		Self {788			allow_different: self.allow_different,789			regenerate_on_owner_added: false,790			regenerate_on_owner_removed: false,791		}792	}793}