git.delta.rocks / jrsonnet / refs/commits / 7b7c4bb80b01

difftreelog

source

crates/fleet-base/src/fleetdata.rs20.6 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	}221222	pub fn prune(&mut self, reason: String) {223		assert!(224			self.pending_prune.is_none(),225			"it shouldn't be possible to prune the same distribution twice using public api"226		);227		self.pending_prune = Some(reason);228	}229	pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {230		// if self.owners.iter().all(|o| owners.contains(o)) && self.owners_pending_prune.is_empty() {231		// 	self.prune(format!("all owners were pruned: {reason}"));232		// 	return;233		// }234		for owner in owners {235			if self.owners.remove(owner) {236				self.owners_pending_prune237					.insert(owner.to_owned(), reason.clone());238			}239		}240		// if self.owners.is_empty() {241		// 	self.prune("no owners left".to_owned());242		// }243	}244	pub fn unprune_owner(&mut self, owner: SecretOwner) {245		if self.owners_pending_prune.remove(&owner).is_some() {246			self.owners.insert(owner);247		}248	}249}250251#[derive(Clone, Debug, Default)]252#[must_use]253pub struct FleetSecretDistributions {254	stored: Vec<FleetSecretDistribution>,255}256257fn compare_dists(258	a: &FleetSecretDistribution,259	b: &FleetSecretDistribution,260	prefer_identities: &BTreeSet<SecretOwner>,261	include_pruned_owners: bool,262) -> Ordering {263	use Ordering::*;264	if prefer_identities.is_empty() {265		let a_has = a266			.owners_ex(include_pruned_owners)267			.any(|o| prefer_identities.contains(o));268		let b_has = b269			.owners_ex(include_pruned_owners)270			.any(|o| prefer_identities.contains(o));271		match (a_has, b_has) {272			(true, false) => return Greater,273			(false, true) => return Less,274			_ => {}275		}276	}277	match (a.secret.expires_at, b.secret.expires_at) {278		(None, Some(_)) => return Greater,279		(Some(_), None) => return Less,280		(Some(a), Some(b)) => {281			// Later is better282			return a.cmp(&b);283		}284		(None, None) => {}285	}286287	// Which one is easier to access288	return a.owners.len().cmp(&b.owners.len());289}290291impl FleetSecretDistributions {292	/// Drop expired distributions293	fn prune_expired(&mut self, now: DateTime<Utc>) {294		for ele in self.distributions_mut() {295			if let Some(expires_at) = ele.secret.expires_at {296				if expires_at < now {297					ele.prune(format!("expired during check at {now}"));298				}299			}300		}301	}302	/// Perform all pruning relevant to shared secrets303	/// Also see expected_owner_removed304	pub fn prune_shared(305		&mut self,306		expected_owners: &BTreeSet<SecretOwner>,307		unique: bool,308		expected_parts: &BTreeMap<String, GeneratorPart>,309		expected_generation_data: &Value,310		regenerate_on_owner_removed: bool,311		regenerate_on_owner_added: bool,312		prefer_identities: &BTreeSet<SecretOwner>,313		now: DateTime<Utc>,314	) {315		self.prune_expired(now);316		self.prune_generation_data(expected_generation_data, None);317		self.prune_missing_parts(expected_parts, None);318319		let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();320321		let mut to_add = expected_owners.difference(&current_owners);322		if to_add.next().is_some() && unique && regenerate_on_owner_added {323			for dist in self.distributions_mut() {324				dist.prune(format!(325					"owners missing, can't add new distribution, regeneration preferred"326				));327			}328			return;329		}330331		for to_remove in current_owners.difference(&expected_owners) {332			self.entry(to_remove.clone()).remove(333				regenerate_on_owner_removed,334				"owner was removed from expected owners list, regenerate_on_owner_removed is set"335					.to_string(),336			);337		}338		if unique {339			self.prune_nonunique(prefer_identities);340		}341	}342	pub fn prune_host(343		&mut self,344		owner: SecretOwner,345		expected_parts: &BTreeMap<String, GeneratorPart>,346		expected_generation_data: &Value,347		now: DateTime<Utc>,348	) {349		self.prune_expired(now);350		self.prune_generation_data(expected_generation_data, Some(&owner));351		// TODO: Owner-based pruning is warranted (e.g host no longer has secret defined)352		self.prune_missing_parts(expected_parts, Some(&owner));353	}354	/// Position of best distributions as in iterator returned by distributions()355	/// None if distributions not found356	fn best_idx(357		&self,358		prefer_identities: &BTreeSet<SecretOwner>,359		include_pruned_owners: bool,360	) -> Option<usize> {361		self.distributions()362			.enumerate()363			.max_by(|(_, a), (_, b)| {364				compare_dists(&a, &b, prefer_identities, include_pruned_owners)365			})366			.map(|(p, _)| p)367	}368	/// Secret wants to be the same on all hosts, leave only one unpruned version of it369	fn prune_nonunique(&mut self, prefer_identities: &BTreeSet<SecretOwner>) {370		if self.distributions().next().is_none() {371			return;372		}373		let best = self.best_idx(prefer_identities, false).expect("not empty");374		for (i, dist) in self.distributions_mut().enumerate() {375			if i != best {376				dist.prune(377					"secret wants to be the same on all hosts, only the best one was left"378						.to_owned(),379				);380			}381		}382	}383384	pub fn try_unprune(&mut self, owner: SecretOwner) -> Option<&FleetSecretDistribution> {385		assert!(self.get(&owner).is_none(), "secret is not pruned for host");386		if let Some(dist) = self387			.distributions_mut()388			.find(|v| v.owners_pending_prune.contains_key(&owner))389		{390			dist.unprune_owner(owner);391			Some(dist)392		} else {393			None394		}395	}396397	pub fn best_distribution_for_reencryption(398		&mut self,399		prefer_identities: &BTreeSet<SecretOwner>,400	) -> Option<&mut FleetSecretDistribution> {401		let best_idx = self.best_idx(prefer_identities, true)?;402		self.distributions_mut().nth(best_idx)403	}404405	fn prune_missing_parts(406		&mut self,407		expected_parts: &BTreeMap<String, GeneratorPart>,408		filter_owner: Option<&SecretOwner>,409	) {410		'dist: for ele in self.distributions_mut() {411			if let Some(filter_owner) = filter_owner {412				if !ele.owners.contains(filter_owner) {413					continue;414				}415				// Note: secret still can have multiple owners even if it is host-owned416				// in this case we expect that all owners using the same generator, so we can prune distribution for all of them417			}418			for (name, part) in expected_parts {419				let Some(stored_part) = ele.secret.parts.get(name) else {420					ele.prune(format!("secret definition added new part: {name}"));421					continue 'dist;422				};423				if part.encrypted != stored_part.raw.encrypted {424					ele.prune(format!(425						"secret definition now requires part to be {}",426						if part.encrypted {427							"encrypted"428						} else {429							"non-encrypted"430						}431					));432					continue 'dist;433				}434			}435		}436	}437	fn prune_generation_data(438		&mut self,439		expected_generation_data: &Value,440		filter_owner: Option<&SecretOwner>,441	) {442		for ele in self.distributions_mut() {443			if let Some(filter_owner) = filter_owner {444				if !ele.owners.contains(filter_owner) {445					continue;446				}447				// Note: secret still can have multiple owners even if it is host-owned448				// in this case we expect that all owners using the same generator, so we can prune distribution for all of them449			}450			if ele.secret.generation_data != *expected_generation_data {451				ele.prune(format!(452					"expected generation data mismatch: {expected_generation_data:?}"453				));454			}455		}456	}457458	/// Prune all distributions with no unpruned owners.459	/// For ease of reencryption where possible, it is only called on persistence, when in memory - pruned owners are kept and460	/// can decrypt their secrets.461	fn prune_dead(&mut self) {462		for ele in self.distributions_mut() {463			if ele.owners.is_empty() {464				ele.prune("no owners left".to_owned());465			}466		}467	}468469	pub fn distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {470		self.stored.iter().filter(|v| v.pending_prune.is_none())471	}472	pub fn distributions_mut(&mut self) -> impl Iterator<Item = &mut FleetSecretDistribution> {473		self.stored.iter_mut().filter(|v| v.pending_prune.is_none())474	}475	pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {476		self.distributions().flat_map(|v| v.owners.iter())477	}478	#[allow(479		clippy::len_without_is_empty,480		reason = "should not be empty for a long time"481	)]482	pub fn len(&self) -> usize {483		self.distributions().count()484	}485486	pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {487		self.distributions().find(|d| d.owners.contains(owner))488	}489	fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {490		let Some((idx, dist)) = self491			.distributions()492			.enumerate()493			.find(|(_, d)| d.owners.contains(&owner))494		else {495			return DistEntry::Vacant(VacantDistEntry {496				distributions: self,497				owners: BTreeSet::from([owner]),498			});499		};500		DistEntry::Occupied(OccupiedDistEntry {501			owners: dist.owners.clone(),502			distributions: self,503			idx,504		})505	}506	pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {507		for ele in self.distributions_mut() {508			ele.prune_owners(&dist.owners, reason.clone());509		}510		self.stored.push(dist);511	}512	pub fn contains(&self, owner: &SecretOwner) -> bool {513		self.distributions().any(|d| d.owners.contains(owner))514	}515}516517struct OccupiedDistEntry<'d> {518	distributions: &'d mut FleetSecretDistributions,519	idx: usize,520	owners: BTreeSet<SecretOwner>,521}522impl<'d> OccupiedDistEntry<'d> {523	fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {524		let dist = &mut self.distributions.stored[self.idx];525		if whole_dist {526			dist.prune(reason);527		} else {528			dist.prune_owners(&self.owners, reason);529		}530		VacantDistEntry {531			distributions: self.distributions,532			owners: self.owners,533		}534	}535	fn set(self, secret: FleetSecretData, reason: String) -> Self {536		self.remove(false, reason).set(secret)537	}538}539struct VacantDistEntry<'d> {540	distributions: &'d mut FleetSecretDistributions,541	owners: BTreeSet<SecretOwner>,542}543impl<'d> VacantDistEntry<'d> {544	fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {545		let Self {546			distributions,547			owners,548		} = self;549		let idx = distributions.stored.len();550		distributions.stored.push(FleetSecretDistribution {551			owners: owners.clone(),552			secret,553554			owners_pending_prune: BTreeMap::new(),555			pending_prune: None,556			_deprecated_managed: true,557		});558		OccupiedDistEntry {559			distributions,560			owners,561			idx,562		}563	}564}565566enum DistEntry<'d> {567	Vacant(VacantDistEntry<'d>),568	Occupied(OccupiedDistEntry<'d>),569}570impl DistEntry<'_> {571	fn remove(self, whole_dist: bool, reason: String) -> Self {572		match self {573			DistEntry::Vacant(_) => self,574			DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),575		}576	}577	fn set(self, secret: FleetSecretData, reason: String) -> Self {578		Self::Occupied(match self {579			DistEntry::Vacant(e) => e.set(secret),580			DistEntry::Occupied(e) => e.set(secret, reason),581		})582	}583}584585impl Serialize for FleetSecretDistributions {586	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>587	where588		S: serde::Serializer,589	{590		let mut v = self.clone();591		v.prune_dead();592		let mut found_hosts = BTreeSet::new();593		for ele in v.distributions() {594			if ele.pending_prune.is_some() {595				continue;596			}597			if ele.owners.is_empty() {598				panic!("consistency: secret distribution has no defined owners");599			}600			for ele in ele.owners.iter() {601				if !found_hosts.insert(ele) {602					panic!(603						"consistency: secret distribution contains duplicate entry for the same host",604					);605				}606			}607		}608		match v.stored.len() {609			0 => panic!("consistency: empty distributions"),610			1 => v.stored[0].serialize(serializer),611			_ => {612				let mut sorted = v.stored.clone();613				// Store outdated distributions last614				sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);615				sorted.serialize(serializer)616			}617		}618	}619}620impl<'de> Deserialize<'de> for FleetSecretDistributions {621	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>622	where623		D: serde::Deserializer<'de>,624	{625		#[derive(Deserialize)]626		#[serde(untagged)]627		enum Distributions {628			One(FleetSecretDistribution),629			Many(Vec<FleetSecretDistribution>),630		}631		let d = Distributions::deserialize(deserializer)?;632		let stored = match d {633			Distributions::One(d) => vec![d],634			Distributions::Many(ds) => ds,635		};636		if stored.is_empty() {637			return Err(de::Error::custom("consistency: empty distributions"));638		}639		let mut found_hosts = BTreeSet::new();640		for ele in stored.iter() {641			if ele.pending_prune.is_some() {642				continue;643			}644			if ele.owners.is_empty() {645				return Err(de::Error::custom(646					"consistency: secret distribution has no defined owners",647				));648			}649			for ele in ele.owners.iter() {650				if !found_hosts.insert(ele) {651					return Err(de::Error::custom(652						"consistency: secret distribution contains duplicate entry for the same host",653					));654				}655			}656		}657		Ok(Self { stored })658	}659}660661#[derive(Deserialize, Default)]662pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);663664impl Serialize for FleetSecrets {665	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>666	where667		S: serde::Serializer,668	{669		let data: BTreeMap<String, FleetSecretDistributions> = self670			.0671			.iter()672			.filter(|(_, v)| !v.stored.is_empty())673			.map(|(k, v)| (k.clone(), v.clone()))674			.collect();675676		data.serialize(serializer)677	}678}679680impl FleetSecrets {681	pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {682		self.0.keys()683	}684685	pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {686		self.0687			.iter()688			.filter(|(_, d)| d.contains(owner))689			.map(|(n, _)| n)690	}691692	pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {693		match self.0.entry(secret) {694			Entry::Vacant(e) => {695				e.insert(FleetSecretDistributions { stored: vec![data] });696			}697			Entry::Occupied(mut e) => {698				let dists = e.get_mut();699				dists.extend(data, "secret data was replaced".to_owned())700			}701		}702	}703	pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {704		self.0.get(secret)705	}706	pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {707		self.0.get_mut(secret)708	}709710	pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {711		self.0712			.entry(secret.to_owned())713			.or_insert(FleetSecretDistributions::default())714	}715716	pub fn contains(&self, secret: &str) -> bool {717		self.0.contains_key(secret)718	}719	pub fn remove(&mut self, secret: &str) {720		self.0.remove(secret);721	}722723	fn merge_from_hosts(724		&mut self,725		host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,726	) {727		for (host, host_secrets) in host_secrets {728			for (secret_name, mut secret_data) in host_secrets {729				secret_data.owners.insert(host.clone());730				self.set_data(secret_name, secret_data);731			}732		}733	}734735	pub fn prune_host(&mut self, host: &SecretOwner, expected_nonshared: BTreeSet<String>) {736		for (name, dists) in self.0.iter_mut() {737			if expected_nonshared.contains(name) {738				continue;739			}740			for dist in dists.distributions_mut() {741				if dist.owners.contains(host) {742					dist.prune_owners(743						&BTreeSet::from([host.to_owned()]),744						"host no longer defines this secret".to_owned(),745					);746				}747			}748		}749	}750}751752#[derive(Debug, Clone)]753pub struct Expectations {754	pub owners: BTreeSet<SecretOwner>,755	pub generation_data: serde_json::Value,756	pub parts: BTreeMap<String, GeneratorPart>,757}758#[derive(Deserialize, Debug, Clone)]759pub struct GeneratorPart {760	pub encrypted: bool,761}762763#[derive(Debug, Clone, Copy)]764pub struct RegenerationConstraints {765	pub allow_different: bool,766	pub regenerate_on_owner_added: bool,767	pub regenerate_on_owner_removed: bool,768}769impl RegenerationConstraints {770	pub fn host_personal() -> Self {771		Self {772			allow_different: false,773			regenerate_on_owner_added: true,774			regenerate_on_owner_removed: true,775		}776	}777	pub fn without_preferences(self) -> Self {778		Self {779			allow_different: self.allow_different,780			regenerate_on_owner_added: false,781			regenerate_on_owner_removed: false,782		}783	}784}