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

difftreelog

source

src/db/secret.rs5.3 KiBsourcehistory
1use crate::{command::CommandExt, host::FleetConfig, nix::SECRETS_ATTRIBUTE};2use anyhow::{bail, 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::db::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		.run_json()35}3637struct ReadableDate(PrimitiveDateTime);38impl Serialize for ReadableDate {39	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>40	where41		S: Serializer,42	{43		serializer.serialize_str(&self.0.to_string())44	}45}46impl<'de> Deserialize<'de> for ReadableDate {47	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>48	where49		D: Deserializer<'de>,50	{51		Ok(Self(52			PrimitiveDateTime::parse(String::deserialize(deserializer)?, "%F %T").unwrap(),53		))54	}55}56impl From<PrimitiveDateTime> for ReadableDate {57	fn from(d: PrimitiveDateTime) -> Self {58		Self(d)59	}60}61impl From<ReadableDate> for PrimitiveDateTime {62	fn from(d: ReadableDate) -> Self {63		d.064	}65}6667#[derive(serde::Serialize, serde::Deserialize)]68struct SecretData {69	created_at: ReadableDate,70	renew_at: Option<ReadableDate>,71	owners: BTreeSet<String>,7273	public_data: BTreeMap<String, String>,74	private_files: BTreeMap<String, String>,75}76impl SecretData {77	fn should_renew(&self) -> bool {78		if let Some(renew_at) = &self.renew_at {79			let now: PrimitiveDateTime = SystemTime::now().into();80			renew_at.0 <= now81		} else {82			false83		}84	}85	fn is_valid(&self, data: &SecretListData) -> bool {86		self.owners == data.owners87	}88}8990#[derive(serde::Serialize, serde::Deserialize)]91struct NixDataValue {92	data: BTreeMap<String, String>,93}9495#[derive(serde::Serialize, serde::Deserialize)]96struct NixData {97	secrets: BTreeMap<String, NixDataValue>,98}99100#[derive(serde::Serialize, serde::Deserialize, Default)]101pub struct SecretDb {102	secrets: BTreeMap<String, SecretData>,103}104impl DbData for SecretDb {105	const DB_NAME: &'static str = "secrets";106}107108impl SecretDb {109	// Secrets are generated on machine running fleet command110	pub fn generate_secret(111		&mut self,112		fleet_config: FleetConfig,113		secret: &str,114		data: &SecretListData,115	) -> Result<()> {116		let mut rage_keys = String::new();117		for (i, owner) in data.owners.iter().enumerate() {118			if i != 0 {119				rage_keys.push(' ');120			}121			rage_keys.push_str("--recipient \"");122			// rage_keys.push_str(&keys.get_host_key(&owner)?);123			rage_keys.push('"')124		}125		let created_at: PrimitiveDateTime = SystemTime::now().into();126		let renew_at = data127			.renew_in128			.map(|hours| created_at + Duration::hours(hours as i64));129		let built = tempfile::tempdir()?;130		Command::new("nix")131			.inherit_stdio()132			.arg("build")133			.arg(format!("{}.{}.generator", SECRETS_ATTRIBUTE, secret))134			.arg("--no-link")135			.arg("--out-link")136			.arg(built.path())137			.arg("--impure")138			.env("RAGE_KEYS", rage_keys)139			.env("IMPURITY_SOURCE", format!("{:?}", Instant::now()))140			.run()?;141		let path = built.path().to_owned();142		let mut secret_data = SecretData {143			created_at: created_at.into(),144			renew_at: renew_at.map(|v| v.into()),145			owners: data.owners.clone(),146			public_data: BTreeMap::new(),147			private_files: BTreeMap::new(),148		};149		for file in std::fs::read_dir(path)? {150			let entry = file?;151			if !entry.file_type()?.is_file() {152				bail!("Secret generator should produce files, not directories");153			}154			let name = entry.file_name();155			let name = name156				.to_str()157				.ok_or(anyhow::anyhow!("file name should be utf-8"))?;158			let value = String::from_utf8(std::fs::read(entry.path())?)?;159			if let Some(name) = name.strip_prefix("pub_") {160				secret_data.public_data.insert(name.into(), value);161			} else {162				secret_data.private_files.insert(name.into(), value);163			}164		}165		self.secrets.insert(secret.into(), secret_data);166		Ok(())167	}168	pub fn need_to_generate(&self, secret: &str, data: &SecretListData) -> Result<bool> {169		let secret = self.secrets.get(secret);170		if secret.is_none() {171			return Ok(true);172		}173		let secret = secret.unwrap();174175		if secret.should_renew() {176			return Ok(true);177		}178179		if !secret.is_valid(&data) {180			return Ok(true);181		}182183		Ok(false)184	}185	pub fn ensure_generated(186		&mut self,187		// keys: &KeyDb,188		secret: &str,189		data: &SecretListData,190	) -> Result<()> {191		if self.need_to_generate(secret, data)? {192			info!("Generating secret {}", secret);193			// self.generate_secret(keys, secret, data)?;194		}195196		Ok(())197	}198	pub fn generate_nix_data(&self) -> Result<String> {199		let mut out = BTreeMap::new();200		for (host, secrets) in &self.secrets {201			out.insert(202				host.to_owned(),203				NixDataValue {204					data: secrets205						.public_data206						.clone()207						.iter()208						.map(|(k, v)| (k.to_owned(), v.trim().to_owned()))209						.collect(),210				},211			);212		}213		Ok(serde_json::to_string(&out)?)214	}215216	pub fn has_secret(&self, secret: &str) -> bool {217		self.secrets.contains_key(secret)218	}219220	pub fn remove_secret(&mut self, secret: &str) {221		self.secrets.remove(secret);222	}223}