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

difftreelog

source

src/db/secret.rs5.5 KiBsourcehistory
1use crate::{command::CommandExt, host::FleetOpts, nix::SECRETS_ATTRIBUTE};2use anyhow::{bail, Context, Result};3use log::info;4use serde::{Deserialize, Deserializer, Serialize, Serializer};5use std::{6	collections::{BTreeMap, BTreeSet, HashMap},7	process::Command,8	time::Instant,9	time::SystemTime,10};11use time::{Duration, PrimitiveDateTime};1213use super::DbData;1415#[derive(Serialize, Deserialize, Debug)]16pub struct SecretListData {17	pub owners: BTreeSet<String>,18	#[serde(rename = "expireIn")]19	renew_in: Option<u64>,20}21pub fn list_secrets() -> Result<HashMap<String, SecretListData>> {22	Command::new("nix")23		.inherit_stdio()24		.arg("eval")25		.arg(SECRETS_ATTRIBUTE)26		.arg("--apply")27		.arg(28			r#"29				s: (builtins.mapAttrs (n: {owners, expireIn, ...}: {30					inherit owners expireIn;31				}) s)32			"#,33		)34		.arg("--json")35		.run_json()36		.context("while getting secret list")37}3839struct ReadableDate(PrimitiveDateTime);40impl Serialize for ReadableDate {41	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>42	where43		S: Serializer,44	{45		serializer.serialize_str(&self.0.to_string())46	}47}48impl<'de> Deserialize<'de> for ReadableDate {49	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>50	where51		D: Deserializer<'de>,52	{53		Ok(Self(54			PrimitiveDateTime::parse(String::deserialize(deserializer)?, "%F %T").unwrap(),55		))56	}57}58impl From<PrimitiveDateTime> for ReadableDate {59	fn from(d: PrimitiveDateTime) -> Self {60		Self(d)61	}62}63impl From<ReadableDate> for PrimitiveDateTime {64	fn from(d: ReadableDate) -> Self {65		d.066	}67}6869#[derive(serde::Serialize, serde::Deserialize)]70struct SecretData {71	created_at: ReadableDate,72	renew_at: Option<ReadableDate>,73	owners: BTreeSet<String>,7475	public_data: BTreeMap<String, String>,76	private_files: BTreeMap<String, String>,77}78impl SecretData {79	fn should_renew(&self) -> bool {80		if let Some(renew_at) = &self.renew_at {81			let now: PrimitiveDateTime = SystemTime::now().into();82			renew_at.0 <= now83		} else {84			false85		}86	}87	fn is_valid(&self, data: &SecretListData) -> bool {88		self.owners == data.owners89	}90}9192#[derive(serde::Serialize, serde::Deserialize)]93struct NixDataValue {94	data: BTreeMap<String, String>,95}9697#[derive(serde::Serialize, serde::Deserialize)]98struct NixData {99	secrets: BTreeMap<String, NixDataValue>,100}101102#[derive(serde::Serialize, serde::Deserialize, Default)]103pub struct SecretDb {104	secrets: BTreeMap<String, SecretData>,105}106impl DbData for SecretDb {107	const DB_NAME: &'static str = "secrets";108}109110impl SecretDb {111	// Secrets are generated on machine running fleet command112	pub fn generate_secret(113		&mut self,114		_fleet_config: &FleetOpts,115		secret: &str,116		data: &SecretListData,117	) -> Result<()> {118		let mut rage_keys = String::new();119		for (i, _owner) in data.owners.iter().enumerate() {120			if i != 0 {121				rage_keys.push(' ');122			}123			rage_keys.push_str("--recipient \"");124			// rage_keys.push_str(&fleet_config.clone().build()?.host(owner)?.key()?);125			//rage_keys.push_str(&keys.get_host_key(&owner)?);126			rage_keys.push('"')127		}128		let created_at: PrimitiveDateTime = SystemTime::now().into();129		let renew_at = data130			.renew_in131			.map(|hours| created_at + Duration::hours(hours as i64));132		let built = tempfile::tempdir()?;133		Command::new("nix")134			.inherit_stdio()135			.arg("build")136			.arg(format!("{}.{}.generator", SECRETS_ATTRIBUTE, secret))137			.arg("--no-link")138			.arg("--out-link")139			.arg(built.path())140			.arg("--impure")141			.env("RAGE_KEYS", rage_keys)142			.env("IMPURITY_SOURCE", format!("{:?}", Instant::now()))143			.run()?;144		let path = built.path().to_owned();145		let mut secret_data = SecretData {146			created_at: created_at.into(),147			renew_at: renew_at.map(|v| v.into()),148			owners: data.owners.clone(),149			public_data: BTreeMap::new(),150			private_files: BTreeMap::new(),151		};152		for file in std::fs::read_dir(path)? {153			let entry = file?;154			if !entry.file_type()?.is_file() {155				bail!("Secret generator should produce files, not directories");156			}157			let name = entry.file_name();158			let name = name159				.to_str()160				.ok_or_else(|| anyhow::anyhow!("file name should be utf-8"))?;161			let value = String::from_utf8(std::fs::read(entry.path())?)?;162			if let Some(name) = name.strip_prefix("pub_") {163				secret_data.public_data.insert(name.into(), value);164			} else {165				secret_data.private_files.insert(name.into(), value);166			}167		}168		self.secrets.insert(secret.into(), secret_data);169		Ok(())170	}171	pub fn need_to_generate(&self, secret: &str, data: &SecretListData) -> Result<bool> {172		let secret = self.secrets.get(secret);173		if secret.is_none() {174			return Ok(true);175		}176		let secret = secret.unwrap();177178		if secret.should_renew() {179			return Ok(true);180		}181182		if !secret.is_valid(data) {183			return Ok(true);184		}185186		Ok(false)187	}188	pub fn ensure_generated(189		&mut self,190		// keys: &KeyDb,191		fleet_config: &FleetOpts,192		secret: &str,193		data: &SecretListData,194	) -> Result<()> {195		if self.need_to_generate(secret, data)? {196			info!("Generating secret {}", secret);197			self.generate_secret(fleet_config, secret, data)?;198		}199200		Ok(())201	}202	pub fn generate_nix_data(&self) -> Result<String> {203		let mut out = BTreeMap::new();204		for (host, secrets) in &self.secrets {205			out.insert(206				host.to_owned(),207				NixDataValue {208					data: secrets209						.public_data210						.clone()211						.iter()212						.map(|(k, v)| (k.to_owned(), v.trim().to_owned()))213						.collect(),214				},215			);216		}217		Ok(serde_json::to_string(&out)?)218	}219220	pub fn has_secret(&self, secret: &str) -> bool {221		self.secrets.contains_key(secret)222	}223224	pub fn remove_secret(&mut self, secret: &str) {225		self.secrets.remove(secret);226	}227}