Skip to main content

merka_vault/
cli.rs

1//! CLI module for the Merka Vault library
2//!
3//! This module implements the command-line interface for the Merka Vault library.
4//! It can use both the actor-based API (preferred) and the direct vault API.
5//!
6//! Architectural constraints:
7//! - The CLI module may use both the actor and vault modules
8//! - The CLI module should prefer using the actor for stateful operations
9
10use anyhow::Result;
11use async_trait::async_trait;
12use clap::{Parser, Subcommand};
13use std::fs;
14use tracing::info;
15
16use crate::interface::VaultInterface;
17use crate::vault::common::VaultStatus;
18use crate::vault::{UnsealResult, VaultError};
19
20// --- Import the new two-step setup logic ---
21use crate::vault::setup_root::{setup_root_vault, RootSetupConfig, RootSetupResult};
22use crate::vault::setup_sub::{setup_sub_vault, SubSetupConfig, SubSetupResult};
23use crate::vault::wizard::run_setup_wizard;
24
25#[derive(Parser)]
26#[command(
27    name = "merka-vault",
28    about = "Vault provisioning CLI (supports unseal, status, and multi-step setup)",
29    version = "0.2.0"
30)]
31pub struct Cli {
32    /// Default root Vault server address (used if subcommands don't override).
33    #[arg(long, default_value = "http://127.0.0.1:8200", env = "ROOT_VAULT_ADDR")]
34    pub vault_addr: String,
35
36    #[command(subcommand)]
37    pub command: Commands,
38}
39
40#[derive(Subcommand)]
41pub enum Commands {
42    /// List all known vaults and their status
43    List,
44
45    /// Unseal Vault.
46    Unseal {
47        /// Provide one or more unseal keys
48        #[arg(long, value_name = "UNSEAL_KEY")]
49        keys: Vec<String>,
50
51        /// Optionally read keys from a file
52        #[arg(long)]
53        keys_file: Option<String>,
54    },
55
56    /// Check Vault status.
57    Status {
58        /// Optionally override which Vault address to check
59        #[arg(long)]
60        vault_addr: Option<String>,
61    },
62
63    /// Interactive setup wizard for vault provisioning
64    Setup,
65
66    /// (Step 1) Set up the root Vault for auto‐unseal, generating an unwrapped token
67    SetupRoot {
68        /// The root Vault's address (override the CLI default)
69        #[arg(long, default_value = "http://127.0.0.1:8200")]
70        root_addr: String,
71
72        /// Shamir shares & threshold
73        #[arg(long, default_value_t = 1)]
74        secret_shares: u8,
75
76        #[arg(long, default_value_t = 1)]
77        secret_threshold: u8,
78
79        /// Transit key name
80        #[arg(long, default_value = "autounseal-key")]
81        key_name: String,
82
83        /// Local or remote mode
84        #[arg(long, default_value = "local")]
85        mode: String,
86
87        /// Optional output file
88        #[arg(long)]
89        output_file: Option<String>,
90    },
91
92    /// (Step 2) Set up the sub Vault for auto‐unseal + intermediate PKI
93    SetupSub {
94        /// Sub Vault address
95        #[arg(long, default_value = "http://127.0.0.1:8202")]
96        sub_addr: String,
97
98        /// Domain name for PKI
99        #[arg(long, default_value = "example.com")]
100        domain: String,
101
102        /// TTL for PKI certs
103        #[arg(long, default_value = "8760h")]
104        ttl: String,
105
106        /// Root Vault address (if needed to sign the intermediate)
107        #[arg(long)]
108        root_addr: Option<String>,
109
110        /// Root Vault token
111        #[arg(long)]
112        root_token: String,
113    },
114
115    /// Get an unwrapped transit token for auto-unseal from a root vault
116    GetTransitToken {
117        /// The root Vault's address
118        #[arg(long, default_value = "http://127.0.0.1:8200")]
119        root_addr: String,
120
121        /// Root Vault token
122        #[arg(long)]
123        root_token: String,
124
125        /// Transit key name
126        #[arg(long, default_value = "autounseal-key")]
127        key_name: String,
128    },
129
130    /// Start the web server with API endpoints for vault management
131    Server {
132        /// The address to listen on
133        #[arg(long, default_value = "127.0.0.1:8080")]
134        listen_addr: String,
135
136        /// The default vault address
137        #[arg(long, default_value = "http://127.0.0.1:8200")]
138        vault_addr: String,
139
140        /// Path to the SQLite database
141        #[arg(long, default_value = "merka_vault.db")]
142        db_path: String,
143    },
144}
145
146// -------------------------------------------------------------------
147// Optionally, your existing VaultCli that implements VaultInterface
148// -------------------------------------------------------------------
149pub struct VaultCli;
150
151#[async_trait]
152impl VaultInterface for VaultCli {
153    async fn check_status(&self, addr: &str) -> Result<VaultStatus, VaultError> {
154        match crate::vault::common::check_vault_status(addr).await {
155            Ok(status) => Ok(status),
156            Err(err) => Err(VaultError::Api(format!("Status check error: {}", err))),
157        }
158    }
159
160    async fn unseal(&self, addr: &str, keys: Vec<String>) -> Result<UnsealResult, VaultError> {
161        match crate::vault::init::unseal_root_vault(addr, keys).await {
162            Ok(unseal_resp) => Ok(unseal_resp),
163            Err(err) => Err(VaultError::Api(format!("Unseal error: {}", err))),
164        }
165    }
166
167    async fn setup_root(
168        &self,
169        addr: &str,
170        secret_shares: u8,
171        secret_threshold: u8,
172        key_name: &str,
173    ) -> Result<String, VaultError> {
174        let config = RootSetupConfig {
175            root_addr: addr.to_string(),
176            secret_shares,
177            secret_threshold,
178            key_name: key_name.to_string(),
179            mode: "local".to_string(),
180            output_file: None,
181        };
182
183        match setup_root_vault(config).await {
184            Ok(RootSetupResult {
185                root_init,
186                unwrapped_token,
187            }) => {
188                info!(
189                    "Root Vault setup is complete. Root token = {}",
190                    root_init.root_token
191                );
192                Ok(unwrapped_token)
193            }
194            Err(e) => Err(VaultError::Api(format!("Root setup error: {}", e))),
195        }
196    }
197
198    async fn setup_sub(
199        &self,
200        root_addr: &str,
201        root_token: &str,
202        sub_addr: &str,
203        domain: &str,
204        ttl: &str,
205    ) -> Result<String, VaultError> {
206        let config = SubSetupConfig {
207            sub_addr: sub_addr.to_string(),
208            domain: domain.to_string(),
209            ttl: ttl.to_string(),
210            root_addr: root_addr.to_string(),
211            root_token: root_token.to_string(),
212        };
213
214        match setup_sub_vault(config).await {
215            Ok(SubSetupResult {
216                sub_init,
217                pki_roles,
218            }) => {
219                info!(
220                    "Sub Vault is auto-unsealed: root token = {}",
221                    sub_init.root_token
222                );
223                Ok(pki_roles.1)
224            }
225            Err(e) => Err(VaultError::Api(format!("Sub setup error: {}", e))),
226        }
227    }
228
229    async fn get_unwrapped_transit_token(
230        &self,
231        root_addr: &str,
232        root_token: &str,
233        key_name: &str,
234    ) -> Result<String, VaultError> {
235        // First, ensure transit auto-unseal is set up
236        if let Err(e) =
237            crate::vault::autounseal::setup_transit_autounseal(root_addr, root_token, key_name)
238                .await
239        {
240            return Err(VaultError::Api(format!(
241                "Failed to setup transit auto-unseal: {}",
242                e
243            )));
244        }
245
246        // Generate a wrapped transit token
247        let wrap_ttl = "300s";
248        let wrapped_token = match crate::vault::transit::generate_wrapped_transit_token(
249            root_addr,
250            root_token,
251            "autounseal", // policy name used in setup_transit_autounseal
252            wrap_ttl,
253        )
254        .await
255        {
256            Ok(token) => token,
257            Err(e) => {
258                return Err(VaultError::Api(format!(
259                    "Failed to generate wrapped token: {}",
260                    e
261                )))
262            }
263        };
264
265        // Unwrap the token
266        let unwrapped_token =
267            match crate::vault::autounseal::unwrap_token(root_addr, &wrapped_token).await {
268                Ok(token) => token,
269                Err(e) => return Err(VaultError::Api(format!("Failed to unwrap token: {}", e))),
270            };
271
272        info!("Got unwrapped token for auto-unseal: {}", unwrapped_token);
273
274        Ok(unwrapped_token)
275    }
276}
277
278// -------------------------------------------------------------------
279// The main CLI runner that processes commands
280// -------------------------------------------------------------------
281/// Run the CLI application
282///
283/// This function implements the CLI logic, using either the actor-based API
284/// or the direct vault API depending on the operation being performed.
285pub async fn run_cli() -> Result<()> {
286    // Parse CLI args
287    let cli = Cli::parse();
288    let vault_cli = VaultCli;
289
290    match cli.command {
291        Commands::Status { vault_addr } => {
292            // Use the vault_addr override if supplied, otherwise use the global cli.vault_addr.
293            let addr = vault_addr.as_deref().unwrap_or(&cli.vault_addr);
294
295            info!("Checking Vault status at {}", addr);
296            let status = vault_cli.check_status(addr).await?;
297
298            println!("Vault Status for {}:", addr);
299            println!("  Initialized: {}", status.initialized);
300            println!("  Sealed: {}", status.sealed);
301            if status.sealed {
302                println!("  Seal Progress: {}/{}", status.progress, status.t);
303            }
304            println!("  Version: {}", status.version);
305        }
306
307        Commands::Unseal { keys, keys_file } => {
308            // Process keys from command line args and/or file
309            let mut all_keys = keys;
310            if let Some(file) = keys_file {
311                let file_content = fs::read_to_string(file)?;
312                let file_keys: Vec<String> = file_content
313                    .lines()
314                    .filter(|l| !l.trim().is_empty())
315                    .map(|l| l.trim().to_string())
316                    .collect();
317                all_keys.extend(file_keys);
318            }
319
320            info!("Unsealing Vault with {} keys", all_keys.len());
321            let unseal_result = vault_cli.unseal(&cli.vault_addr, all_keys).await?;
322
323            println!("Unseal operation result:");
324            println!("  Sealed: {}", unseal_result.sealed);
325            println!(
326                "  Progress: {}/{}",
327                unseal_result.progress, unseal_result.threshold
328            );
329            if !unseal_result.sealed {
330                println!("  Success: Vault is now unsealed!");
331            } else {
332                println!(
333                    "  Additional keys needed: {} more",
334                    unseal_result.threshold - unseal_result.progress
335                );
336            }
337        }
338
339        Commands::SetupRoot {
340            root_addr,
341            secret_shares,
342            secret_threshold,
343            key_name,
344            mode,
345            output_file,
346        } => {
347            let config = RootSetupConfig {
348                root_addr,
349                secret_shares,
350                secret_threshold,
351                key_name,
352                mode,
353                output_file,
354            };
355
356            let result = setup_root_vault(config).await?;
357            println!(
358                "Root setup complete! Root token: {}",
359                result.root_init.root_token
360            );
361            println!("Unwrapped token for sub-vault: {}", result.unwrapped_token);
362        }
363
364        Commands::SetupSub {
365            root_addr,
366            root_token,
367            sub_addr,
368            domain,
369            ttl,
370        } => {
371            let root_addr = root_addr.unwrap_or_else(|| cli.vault_addr.clone());
372            let config = SubSetupConfig {
373                sub_addr,
374                domain,
375                ttl,
376                root_addr,
377                root_token,
378            };
379
380            let result = setup_sub_vault(config).await?;
381            println!(
382                "Sub vault setup complete! Root token: {}",
383                result.sub_init.root_token
384            );
385            println!("PKI role: {}", result.pki_roles.1);
386        }
387
388        Commands::Setup => {
389            println!("Starting the interactive setup wizard...");
390            match run_setup_wizard().await {
391                Ok(result) => {
392                    println!("Setup wizard completed successfully!");
393                    if let Some(root_result) = result.root_result {
394                        println!("Root Vault setup: SUCCESS");
395                        println!("Root token: {}", root_result.root_init.root_token);
396                    }
397                    if let Some(sub_result) = result.sub_result {
398                        println!("Sub Vault setup: SUCCESS");
399                        println!("Sub token: {}", sub_result.sub_init.root_token);
400                    }
401                }
402                Err(e) => {
403                    println!("Setup wizard encountered an error: {}", e);
404                    return Err(e);
405                }
406            }
407        }
408
409        Commands::GetTransitToken {
410            root_addr,
411            root_token,
412            key_name,
413        } => {
414            let token = vault_cli
415                .get_unwrapped_transit_token(&root_addr, &root_token, &key_name)
416                .await?;
417            println!("Unwrapped transit token: {}", token);
418        }
419
420        Commands::List => {
421            // The list command could check for vaults in known locations or
422            // vaults configured in a config file.
423            println!("Known vaults:");
424            println!("  Root vault: {}", cli.vault_addr);
425            match vault_cli.check_status(&cli.vault_addr).await {
426                Ok(status) => {
427                    println!("    Initialized: {}", status.initialized);
428                    println!("    Sealed: {}", status.sealed);
429                }
430                Err(e) => {
431                    println!("    Status: Error - {}", e);
432                }
433            }
434
435            // Try the sub vault at standard port
436            let sub_addr = cli.vault_addr.replace(":8200", ":8202");
437            match vault_cli.check_status(&sub_addr).await {
438                Ok(status) => {
439                    println!("  Sub vault: {}", sub_addr);
440                    println!("    Initialized: {}", status.initialized);
441                    println!("    Sealed: {}", status.sealed);
442                }
443                Err(_) => {
444                    println!("  Sub vault: Not detected at {}", sub_addr);
445                }
446            }
447        }
448
449        Commands::Server {
450            listen_addr,
451            vault_addr,
452            db_path,
453        } => {
454            println!("Starting the web server on {}...", listen_addr);
455            println!("Using vault at: {}", vault_addr);
456            println!("Database path: {}", db_path);
457
458            // Run the server (this is a blocking call)
459            crate::server::start_server(&listen_addr, &vault_addr, &db_path).await?;
460        }
461    }
462
463    Ok(())
464}