1use std::{2 collections::{BTreeSet, HashSet},3 io::{Read, stdin},4 path::PathBuf,5};67use anyhow::{Result, bail, ensure};8use clap::Parser;9use fleet_base::{host::Config, opts::FleetOpts};10use fleet_shared::SecretData;11use tokio::fs::read;12use tracing::{info, warn};1314#[derive(Parser)]15pub enum Secret {16 /// Force load host keys for all defined hosts17 ForceKeys,18 /// Read secret from remote host, requires sudo on one of the owning hosts19 Read {20 /// Secret name to read21 name: String,2223 /// Distribution with what machine to read24 /// If not shared between multiple - defaults to single owner25 #[clap(short = 'm', long)]26 machine: Option<String>,2728 /// Which private secret part to read29 #[clap(short = 'p', long, default_value = "secret")]30 part: Option<String>,3132 /// Which host should we use to decrypt, in case if reencryption is required, without33 /// regeneration34 #[clap(long)]35 prefer_identities: Vec<String>,36 },37 /// Prune (remove, mark for regeneration) secrets38 Prune {39 /// Secret to prune40 name: String,4142 /// Machines to prune - if specified, only the choosen machines will be pruned43 #[clap(short = 'm', long)]44 machine: Vec<String>,45 },46 /// Ensure secret is generated and not expired47 Ensure {48 /// Secret to ensure generated49 name: String,5051 /// Machines to force secret for52 #[clap(short = 'm', long)]53 machine: Vec<String>,54 },55 List {},56 Edit {57 name: String,58 #[clap(short = 'm', long)]59 machine: String,6061 #[clap(long)]62 add: bool,6364 /// Which private secret part to read65 #[clap(short = 'p', long, default_value = "secret")]66 part: String,67 },68}6970async fn parse_public(71 public: Option<String>,72 public_file: Option<PathBuf>,73) -> Result<Option<SecretData>> {74 Ok(match (public, public_file) {75 (Some(v), None) => Some(SecretData {76 data: v.into(),77 encrypted: false,78 }),79 (None, Some(v)) => Some(SecretData {80 data: read(v).await?,81 encrypted: false,82 }),83 (Some(_), Some(_)) => {84 bail!("only public or public_file should be set")85 }86 (None, None) => None,87 })88}8990async fn parse_secret() -> Result<Option<Vec<u8>>> {91 let mut input = vec![];92 stdin().read_to_end(&mut input)?;93 if input.is_empty() {94 Ok(None)95 } else {96 Ok(Some(input))97 }98}99100fn parse_machines(101 initial: BTreeSet<String>,102 machines: Option<Vec<String>>,103 mut add_machines: Vec<String>,104 mut remove_machines: Vec<String>,105) -> Result<BTreeSet<String>> {106 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {107 bail!("no operation");108 }109110 let initial_machines = initial.clone();111 let mut target_machines = initial;112 info!("Currently encrypted for {initial_machines:?}");113114 if let Some(machines) = machines {115 ensure!(116 add_machines.is_empty() && remove_machines.is_empty(),117 "can't combine --machines and --add-machines/--remove-machines"118 );119 let target = initial_machines.iter().collect::<HashSet<_>>();120 let source = machines.iter().collect::<HashSet<_>>();121 for removed in target.difference(&source) {122 remove_machines.push((*removed).clone());123 }124 for added in source.difference(&target) {125 add_machines.push((*added).clone());126 }127 }128129 for machine in &remove_machines {130 if !target_machines.remove(machine) {131 warn!("secret is not enabled for {machine}");132 }133 }134 for machine in &add_machines {135 if !target_machines.insert(machine.to_owned()) {136 warn!("secret is already added to {machine}");137 }138 }139 if !remove_machines.is_empty() {140 // TODO: maybe force secret regeneration?141 // Not that useful without revokation.142 warn!(143 "secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"144 );145 }146 Ok(target_machines)147}148impl Secret {149 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {150 match self {151 Secret::ForceKeys => {152 for host in config.list_hosts()? {153 if opts.should_skip(&host)? {154 continue;155 }156 config.host_key(&host.name).await?;157 }158 }159 Secret::Read {160 name,161 machine,162 part: part_name,163 mut prefer_identities,164 } => {165 /*166 let Some(secret) = config.shared_secret(&name) else {167 bail!("secret doesn't exists");168 };169170 let dist = if secret.len() == 1 {171 &secret[0]172 } else if let Some(machine) = machine {173 let dist = secret.get(&machine);174 let Some(dist) = dist else {175 bail!("machine {machine} has no distribution of secret {name}");176 };177 prefer_identities.push(machine);178 dist179 } else {180 bail!(181 "secret {name} has shares, but no --machine specified for specifing which do you need"182 )183 };184185 let Some(part) = dist.secret.parts.get(&part_name) else {186 bail!("no part {part_name} in secret {name}");187 };188 let data = if part.raw.encrypted {189 let identity_holder = if !prefer_identities.is_empty() {190 prefer_identities191 .iter()192 .find(|i| dist.owners.iter().any(|s| s == *i))193 } else {194 dist.owners.first()195 };196 let Some(identity_holder) = identity_holder else {197 bail!("no available holder found");198 };199 let host = config.host(identity_holder)?;200 host.decrypt(part.raw.clone()).await?201 } else {202 part.raw.data.clone()203 };204 stdout().write_all(&data)?;205 */206 todo!()207 }208 Secret::List {} => {209 /*210 let _span = info_span!("loading secrets").entered();211 let configured = config.list_configured_shared()?;212 #[derive(Tabled)]213 struct SecretDisplay {214 #[tabled(rename = "Name")]215 name: String,216 #[tabled(rename = "Owners")]217 owners: String,218 }219 // let mut table = vec![];220 for name in configured.iter().cloned() {221 let config = config.clone();222 let data = config.shared_secret(&name).expect("exists");223 /*224 let definition = config.shared_secret_definition(&name)?;225 let expectations = definition.expectations()?;226 let owners = data227 .owners()228 .map(|o| {229 if expectations.owners.contains(o) {230 o.green().to_string()231 } else {232 o.red().to_string()233 }234 })235 .collect::<Vec<_>>();236 table.push(SecretDisplay {237 owners: owners.join(", "),238 name,239 })240 */241 }242 // info!("loaded\n{}", Table::new(table).to_string())243 */244 todo!()245 }246 Secret::Edit {247 name,248 machine,249 part,250 add,251 } => {252 /*let secret = config253 .host_secret(&machine, &name)254 .context("secret not found")?;255 if let Some(data) = secret.secret.parts.get(&part) {256 let host = config.host(&machine)?;257 let secret = host.decrypt(data.raw.clone()).await?;258 String::from_utf8(secret).context("secret is not utf8")?259 } else if add {260 String::new()261 } else {262 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");263 };*/264 todo!()265 }266 Secret::Prune { name, machine } => todo!(),267 Secret::Ensure { name, machine } => todo!(),268 }269 Ok(())270 }271}272273/*274async fn edit_temp_file(275 builder: tempfile::Builder<'_, '_>,276 r: Vec<u8>,277 header: &str,278 comment: &str,279) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {280 if !stdin().is_tty() {281 // TODO: Also try to open /dev/tty directly?282 bail!("stdin is not tty, can't open editor");283 }284285 use std::fmt::Write;286 let mut file = builder.tempfile()?;287288 let mut full_header = String::new();289 let mut had = false;290 for line in header.trim_end().lines() {291 had = true;292 writeln!(&mut full_header, "{comment}{line}")?;293 }294 if had {295 writeln!(&mut full_header, "{}", comment.trim_end())?;296 }297 writeln!(298 &mut full_header,299 "{comment}Do not touch this header! It will be removed automatically"300 )?;301302 file.write_all(full_header.as_bytes())?;303 file.write_all(&r)?;304305 let abs_path = file.into_temp_path();306 let editor = std::env::var_os("VISUAL")307 .or_else(|| std::env::var_os("EDITOR"))308 .unwrap_or_else(|| "vi".into());309 let editor_args = shlex::bytes::split(editor.as_encoded_bytes())310 .ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;311 let editor_args = editor_args312 .into_iter()313 .map(|v| {314 // Only ASCII subsequences are replaced315 unsafe { OsString::from_encoded_bytes_unchecked(v) }316 })317 .collect_vec();318 let Some((editor, args)) = editor_args.split_first() else {319 bail!("EDITOR env var has no command");320 };321 let mut command = Command::new(editor);322 command.args(args);323324 let path_arg = abs_path.canonicalize()?;325326 // TODO: Save full state, using tcget/_getmode/_setmode327 let was_raw = terminal::is_raw_mode_enabled()?;328 terminal::enable_raw_mode()?;329330 let status = command.arg(path_arg).status().await;331332 if !was_raw {333 terminal::disable_raw_mode()?;334 }335336 let success = match status {337 Ok(s) => s.success(),338 Err(e) if e.kind() == io::ErrorKind::NotFound => {339 bail!("editor not found")340 }341 Err(e) => bail!("editor spawn error: {e}"),342 };343344 let mut file = std::fs::read(&abs_path).context("read editor output")?;345 let Some(v) = file.strip_prefix(full_header.as_bytes()) else {346 todo!();347 };348 todo!();349350 // Ok((success, abs_path))351}352*/difftreelog
source
cmds/fleet/src/cmds/secrets/mod.rs8.9 KiBsourcehistory