diff --git a/src/commands/app.rs b/src/commands/app.rs index b524a5d..5d6695e 100644 --- a/src/commands/app.rs +++ b/src/commands/app.rs @@ -13,8 +13,6 @@ use crate::cli::prompt::Prompt; use crate::commands::init::{PATH_CA_TRUST, SECRET_ID_TTL, TOKEN_TTL}; use crate::i18n::Messages; use crate::state::{AppEntry, AppRoleEntry, DeployType, StateFile}; - -const STATE_FILE_NAME: &str = "state.json"; const APPROLE_PREFIX: &str = "bootroot-app-"; const APP_KV_BASE: &str = "bootroot/apps"; const APP_SECRET_DIR: &str = "apps"; @@ -23,12 +21,12 @@ const APP_SECRET_ID_FILENAME: &str = "secret_id"; const CA_TRUST_KEY: &str = "trusted_ca_sha256"; pub(crate) async fn run_app_add(args: &AppAddArgs, messages: &Messages) -> Result<()> { - let state_path = Path::new(STATE_FILE_NAME); + let state_path = StateFile::default_path(); if !state_path.exists() { anyhow::bail!(messages.error_state_missing()); } let mut state = - StateFile::load(state_path).with_context(|| messages.error_parse_state_failed())?; + StateFile::load(&state_path).with_context(|| messages.error_parse_state_failed())?; let resolved = resolve_app_add_args(args, messages)?; if state.apps.contains_key(&resolved.service_name) { @@ -106,7 +104,7 @@ pub(crate) async fn run_app_add(args: &AppAddArgs, messages: &Messages) -> Resul .apps .insert(resolved.service_name.clone(), entry.clone()); state - .save(state_path) + .save(&state_path) .with_context(|| messages.error_serialize_state_failed())?; print_app_add_summary( @@ -119,11 +117,12 @@ pub(crate) async fn run_app_add(args: &AppAddArgs, messages: &Messages) -> Resul } pub(crate) fn run_app_info(args: &AppInfoArgs, messages: &Messages) -> Result<()> { - let state_path = Path::new(STATE_FILE_NAME); + let state_path = StateFile::default_path(); if !state_path.exists() { anyhow::bail!(messages.error_state_missing()); } - let state = StateFile::load(state_path).with_context(|| messages.error_parse_state_failed())?; + let state = + StateFile::load(&state_path).with_context(|| messages.error_parse_state_failed())?; let entry = state .apps .get(&args.service_name) diff --git a/src/commands/init/constants.rs b/src/commands/init/constants.rs new file mode 100644 index 0000000..6aebfbf --- /dev/null +++ b/src/commands/init/constants.rs @@ -0,0 +1,59 @@ +pub(crate) const DEFAULT_OPENBAO_URL: &str = "http://localhost:8200"; +pub(crate) const DEFAULT_KV_MOUNT: &str = "secret"; +pub(crate) const DEFAULT_SECRETS_DIR: &str = "secrets"; +pub(crate) const DEFAULT_COMPOSE_FILE: &str = "docker-compose.yml"; +pub(crate) const DEFAULT_STEPCA_URL: &str = "https://localhost:9000"; +pub(crate) const DEFAULT_STEPCA_PROVISIONER: &str = "acme"; + +pub(crate) const DEFAULT_CA_NAME: &str = "Bootroot CA"; +pub(crate) const DEFAULT_CA_PROVISIONER: &str = "admin"; +pub(crate) const DEFAULT_CA_DNS: &str = "localhost,bootroot-ca"; +pub(crate) const DEFAULT_CA_ADDRESS: &str = ":9000"; +pub(crate) const SECRET_BYTES: usize = 32; +pub(crate) const DEFAULT_RESPONDER_TOKEN_TTL_SECS: u64 = 60; +pub(crate) const DEFAULT_RESPONDER_ADMIN_URL: &str = "http://bootroot-http01:8080"; +pub(crate) const RESPONDER_TEMPLATE_DIR: &str = "templates"; +pub(crate) const RESPONDER_TEMPLATE_NAME: &str = "responder.toml.ctmpl"; +pub(crate) const RESPONDER_CONFIG_DIR: &str = "responder"; +pub(crate) const RESPONDER_CONFIG_NAME: &str = "responder.toml"; +pub(crate) const RESPONDER_COMPOSE_OVERRIDE_NAME: &str = "docker-compose.responder.override.yml"; +pub(crate) const STEPCA_PASSWORD_TEMPLATE_NAME: &str = "password.txt.ctmpl"; +pub(crate) const STEPCA_CA_JSON_TEMPLATE_NAME: &str = "ca.json.ctmpl"; +pub(crate) const OPENBAO_AGENT_DIR: &str = "openbao"; +pub(crate) const OPENBAO_AGENT_STEPCA_DIR: &str = "stepca"; +pub(crate) const OPENBAO_AGENT_RESPONDER_DIR: &str = "responder"; +pub(crate) const OPENBAO_AGENT_CONFIG_NAME: &str = "agent.hcl"; +pub(crate) const OPENBAO_AGENT_ROLE_ID_NAME: &str = "role_id"; +pub(crate) const OPENBAO_AGENT_SECRET_ID_NAME: &str = "secret_id"; +pub(crate) const OPENBAO_AGENT_COMPOSE_OVERRIDE_NAME: &str = + "docker-compose.openbao-agent.override.yml"; +pub(crate) const OPENBAO_AGENT_STEPCA_SERVICE: &str = "openbao-agent-stepca"; +pub(crate) const OPENBAO_AGENT_RESPONDER_SERVICE: &str = "openbao-agent-responder"; +pub(crate) const DEFAULT_EAB_ENDPOINT_PATH: &str = "eab"; +pub(crate) const DEFAULT_DB_USER: &str = "stepca"; +pub(crate) const DEFAULT_DB_NAME: &str = "stepca"; +pub(crate) const CA_CERTS_DIR: &str = "certs"; +pub(crate) const CA_ROOT_CERT_FILENAME: &str = "root_ca.crt"; +pub(crate) const CA_INTERMEDIATE_CERT_FILENAME: &str = "intermediate_ca.crt"; +pub(crate) const CA_TRUST_KEY: &str = "trusted_ca_sha256"; + +pub(crate) mod openbao_constants { + pub(crate) const INIT_SECRET_SHARES: u8 = 3; + pub(crate) const INIT_SECRET_THRESHOLD: u8 = 2; + pub(crate) const TOKEN_TTL: &str = "1h"; + pub(crate) const SECRET_ID_TTL: &str = "24h"; + + pub(crate) const POLICY_BOOTROOT_AGENT: &str = "bootroot-agent"; + pub(crate) const POLICY_BOOTROOT_RESPONDER: &str = "bootroot-responder"; + pub(crate) const POLICY_BOOTROOT_STEPCA: &str = "bootroot-stepca"; + + pub(crate) const APPROLE_BOOTROOT_AGENT: &str = "bootroot-agent-role"; + pub(crate) const APPROLE_BOOTROOT_RESPONDER: &str = "bootroot-responder-role"; + pub(crate) const APPROLE_BOOTROOT_STEPCA: &str = "bootroot-stepca-role"; + + pub(crate) const PATH_STEPCA_PASSWORD: &str = "bootroot/stepca/password"; + pub(crate) const PATH_STEPCA_DB: &str = "bootroot/stepca/db"; + pub(crate) const PATH_RESPONDER_HMAC: &str = "bootroot/responder/hmac"; + pub(crate) const PATH_AGENT_EAB: &str = "bootroot/agent/eab"; + pub(crate) const PATH_CA_TRUST: &str = "bootroot/ca"; +} diff --git a/src/commands/init/mod.rs b/src/commands/init/mod.rs new file mode 100644 index 0000000..c1d2716 --- /dev/null +++ b/src/commands/init/mod.rs @@ -0,0 +1,15 @@ +mod constants; +mod paths; +mod steps; +mod types; + +pub(crate) use constants::openbao_constants::{ + INIT_SECRET_SHARES, INIT_SECRET_THRESHOLD, PATH_AGENT_EAB, PATH_CA_TRUST, PATH_RESPONDER_HMAC, + PATH_STEPCA_DB, PATH_STEPCA_PASSWORD, SECRET_ID_TTL, TOKEN_TTL, +}; +pub(crate) use constants::{ + DEFAULT_COMPOSE_FILE, DEFAULT_KV_MOUNT, DEFAULT_OPENBAO_URL, DEFAULT_SECRETS_DIR, + DEFAULT_STEPCA_PROVISIONER, DEFAULT_STEPCA_URL, +}; +pub(crate) use steps::run_init; +pub(crate) use types::{DbCheckStatus, InitPlan, InitSummary, ResponderCheck, StepCaInitResult}; diff --git a/src/commands/init/paths.rs b/src/commands/init/paths.rs new file mode 100644 index 0000000..bfe21bd --- /dev/null +++ b/src/commands/init/paths.rs @@ -0,0 +1,69 @@ +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; + +use super::constants::DEFAULT_RESPONDER_ADMIN_URL; +use crate::cli::args::InitArgs; +use crate::i18n::Messages; + +pub(crate) struct ResponderPaths { + pub(crate) template_path: PathBuf, + pub(crate) config_path: PathBuf, +} + +pub(crate) struct StepCaTemplatePaths { + pub(crate) password_template_path: PathBuf, + pub(crate) ca_json_template_path: PathBuf, +} + +pub(crate) struct OpenBaoAgentPaths { + pub(crate) stepca_agent_config: PathBuf, + pub(crate) responder_agent_config: PathBuf, + pub(crate) compose_override_path: Option, +} + +pub(crate) fn to_container_path( + secrets_dir: &Path, + path: &Path, + messages: &Messages, +) -> Result { + let relative = path + .strip_prefix(secrets_dir) + .with_context(|| messages.error_resolve_path_failed(&path.display().to_string()))?; + Ok(format!("/openbao/secrets/{}", relative.to_string_lossy())) +} + +pub(crate) fn compose_has_responder(compose_file: &Path, messages: &Messages) -> Result { + let compose_contents = std::fs::read_to_string(compose_file) + .with_context(|| messages.error_read_file_failed(&compose_file.display().to_string()))?; + Ok(compose_contents.contains("bootroot-http01")) +} + +pub(crate) fn compose_has_openbao(compose_file: &Path, messages: &Messages) -> Result { + let compose_contents = std::fs::read_to_string(compose_file) + .with_context(|| messages.error_read_file_failed(&compose_file.display().to_string()))?; + Ok(compose_contents.contains("openbao")) +} + +pub(crate) fn resolve_responder_url( + args: &InitArgs, + compose_has_responder: bool, +) -> Option { + if let Some(responder_url) = args.responder_url.as_ref() { + return Some(responder_url.clone()); + } + if compose_has_responder { + Some(DEFAULT_RESPONDER_ADMIN_URL.to_string()) + } else { + None + } +} + +pub(crate) fn resolve_openbao_agent_addr(openbao_url: &str, compose_has_openbao: bool) -> String { + if !compose_has_openbao { + return openbao_url.to_string(); + } + openbao_url + .replace("localhost", "openbao") + .replace("127.0.0.1", "openbao") +} diff --git a/src/commands/init.rs b/src/commands/init/steps.rs similarity index 91% rename from src/commands/init.rs rename to src/commands/init/steps.rs index a682496..6ce6014 100644 --- a/src/commands/init.rs +++ b/src/commands/init/steps.rs @@ -16,6 +16,31 @@ use reqwest::StatusCode; use ring::digest; use x509_parser::pem::parse_x509_pem; +use super::constants::openbao_constants::{ + APPROLE_BOOTROOT_AGENT, APPROLE_BOOTROOT_RESPONDER, APPROLE_BOOTROOT_STEPCA, + INIT_SECRET_SHARES, INIT_SECRET_THRESHOLD, PATH_AGENT_EAB, PATH_CA_TRUST, PATH_RESPONDER_HMAC, + PATH_STEPCA_DB, PATH_STEPCA_PASSWORD, POLICY_BOOTROOT_AGENT, POLICY_BOOTROOT_RESPONDER, + POLICY_BOOTROOT_STEPCA, SECRET_ID_TTL, TOKEN_TTL, +}; +use super::constants::{ + CA_CERTS_DIR, CA_INTERMEDIATE_CERT_FILENAME, CA_ROOT_CERT_FILENAME, CA_TRUST_KEY, + DEFAULT_CA_ADDRESS, DEFAULT_CA_DNS, DEFAULT_CA_NAME, DEFAULT_CA_PROVISIONER, DEFAULT_DB_NAME, + DEFAULT_DB_USER, DEFAULT_EAB_ENDPOINT_PATH, DEFAULT_RESPONDER_TOKEN_TTL_SECS, + OPENBAO_AGENT_COMPOSE_OVERRIDE_NAME, OPENBAO_AGENT_CONFIG_NAME, OPENBAO_AGENT_DIR, + OPENBAO_AGENT_RESPONDER_DIR, OPENBAO_AGENT_RESPONDER_SERVICE, OPENBAO_AGENT_ROLE_ID_NAME, + OPENBAO_AGENT_SECRET_ID_NAME, OPENBAO_AGENT_STEPCA_DIR, OPENBAO_AGENT_STEPCA_SERVICE, + RESPONDER_COMPOSE_OVERRIDE_NAME, RESPONDER_CONFIG_DIR, RESPONDER_CONFIG_NAME, + RESPONDER_TEMPLATE_DIR, RESPONDER_TEMPLATE_NAME, SECRET_BYTES, STEPCA_CA_JSON_TEMPLATE_NAME, + STEPCA_PASSWORD_TEMPLATE_NAME, +}; +use super::paths::{ + OpenBaoAgentPaths, ResponderPaths, StepCaTemplatePaths, compose_has_openbao, + compose_has_responder, resolve_openbao_agent_addr, resolve_responder_url, to_container_path, +}; +use super::types::{ + AppRoleOutput, DbCheckStatus, EabCredentials, InitPlan, InitSummary, ResponderCheck, + StepCaInitResult, +}; use crate::cli::args::InitArgs; use crate::cli::output::{print_init_plan, print_init_summary}; use crate::commands::infra::{ensure_infra_ready, run_docker}; @@ -23,72 +48,6 @@ use crate::commands::openbao_unseal::read_unseal_keys_from_file; use crate::i18n::Messages; use crate::state::StateFile; -pub(crate) const DEFAULT_OPENBAO_URL: &str = "http://localhost:8200"; -pub(crate) const DEFAULT_KV_MOUNT: &str = "secret"; -pub(crate) const DEFAULT_SECRETS_DIR: &str = "secrets"; -pub(crate) const DEFAULT_COMPOSE_FILE: &str = "docker-compose.yml"; -pub(crate) const DEFAULT_STEPCA_URL: &str = "https://localhost:9000"; -pub(crate) const DEFAULT_STEPCA_PROVISIONER: &str = "acme"; - -const DEFAULT_CA_NAME: &str = "Bootroot CA"; -const DEFAULT_CA_PROVISIONER: &str = "admin"; -const DEFAULT_CA_DNS: &str = "localhost,bootroot-ca"; -const DEFAULT_CA_ADDRESS: &str = ":9000"; -const SECRET_BYTES: usize = 32; -const DEFAULT_RESPONDER_TOKEN_TTL_SECS: u64 = 60; -const DEFAULT_RESPONDER_ADMIN_URL: &str = "http://bootroot-http01:8080"; -const RESPONDER_TEMPLATE_DIR: &str = "templates"; -const RESPONDER_TEMPLATE_NAME: &str = "responder.toml.ctmpl"; -const RESPONDER_CONFIG_DIR: &str = "responder"; -const RESPONDER_CONFIG_NAME: &str = "responder.toml"; -const RESPONDER_COMPOSE_OVERRIDE_NAME: &str = "docker-compose.responder.override.yml"; -const STEPCA_PASSWORD_TEMPLATE_NAME: &str = "password.txt.ctmpl"; -const STEPCA_CA_JSON_TEMPLATE_NAME: &str = "ca.json.ctmpl"; -const OPENBAO_AGENT_DIR: &str = "openbao"; -const OPENBAO_AGENT_STEPCA_DIR: &str = "stepca"; -const OPENBAO_AGENT_RESPONDER_DIR: &str = "responder"; -const OPENBAO_AGENT_CONFIG_NAME: &str = "agent.hcl"; -const OPENBAO_AGENT_ROLE_ID_NAME: &str = "role_id"; -const OPENBAO_AGENT_SECRET_ID_NAME: &str = "secret_id"; -const OPENBAO_AGENT_COMPOSE_OVERRIDE_NAME: &str = "docker-compose.openbao-agent.override.yml"; -const OPENBAO_AGENT_STEPCA_SERVICE: &str = "openbao-agent-stepca"; -const OPENBAO_AGENT_RESPONDER_SERVICE: &str = "openbao-agent-responder"; -const DEFAULT_EAB_ENDPOINT_PATH: &str = "eab"; -const DEFAULT_DB_USER: &str = "stepca"; -const DEFAULT_DB_NAME: &str = "stepca"; -const CA_CERTS_DIR: &str = "certs"; -const CA_ROOT_CERT_FILENAME: &str = "root_ca.crt"; -const CA_INTERMEDIATE_CERT_FILENAME: &str = "intermediate_ca.crt"; -const CA_TRUST_KEY: &str = "trusted_ca_sha256"; - -mod openbao_constants { - pub(crate) const INIT_SECRET_SHARES: u8 = 3; - pub(crate) const INIT_SECRET_THRESHOLD: u8 = 2; - pub(crate) const TOKEN_TTL: &str = "1h"; - pub(crate) const SECRET_ID_TTL: &str = "24h"; - - pub(crate) const POLICY_BOOTROOT_AGENT: &str = "bootroot-agent"; - pub(crate) const POLICY_BOOTROOT_RESPONDER: &str = "bootroot-responder"; - pub(crate) const POLICY_BOOTROOT_STEPCA: &str = "bootroot-stepca"; - - pub(crate) const APPROLE_BOOTROOT_AGENT: &str = "bootroot-agent-role"; - pub(crate) const APPROLE_BOOTROOT_RESPONDER: &str = "bootroot-responder-role"; - pub(crate) const APPROLE_BOOTROOT_STEPCA: &str = "bootroot-stepca-role"; - - pub(crate) const PATH_STEPCA_PASSWORD: &str = "bootroot/stepca/password"; - pub(crate) const PATH_STEPCA_DB: &str = "bootroot/stepca/db"; - pub(crate) const PATH_RESPONDER_HMAC: &str = "bootroot/responder/hmac"; - pub(crate) const PATH_AGENT_EAB: &str = "bootroot/agent/eab"; - pub(crate) const PATH_CA_TRUST: &str = "bootroot/ca"; -} - -pub(crate) use openbao_constants::{ - APPROLE_BOOTROOT_AGENT, APPROLE_BOOTROOT_RESPONDER, APPROLE_BOOTROOT_STEPCA, - INIT_SECRET_SHARES, INIT_SECRET_THRESHOLD, PATH_AGENT_EAB, PATH_CA_TRUST, PATH_RESPONDER_HMAC, - PATH_STEPCA_DB, PATH_STEPCA_PASSWORD, POLICY_BOOTROOT_AGENT, POLICY_BOOTROOT_RESPONDER, - POLICY_BOOTROOT_STEPCA, SECRET_ID_TTL, TOKEN_TTL, -}; - pub(crate) async fn run_init(args: &InitArgs, messages: &Messages) -> Result<()> { ensure_infra_ready(&args.compose.compose_file, messages)?; @@ -133,7 +92,7 @@ async fn run_init_inner( .join("config") .join("ca.json") .exists(); - let overwrite_state = Path::new("state.json").exists(); + let overwrite_state = StateFile::default_path().exists(); let plan = InitPlan { openbao_url: args.openbao.openbao_url.clone(), kv_mount: args.openbao.kv_mount.clone(), @@ -284,28 +243,6 @@ struct EabAutoResponse { hmac: String, } -#[derive(Debug, Clone, Copy)] -pub(crate) enum ResponderCheck { - Skipped, - Ok, -} - -struct ResponderPaths { - template_path: PathBuf, - config_path: PathBuf, -} - -struct StepCaTemplatePaths { - password_template_path: PathBuf, - ca_json_template_path: PathBuf, -} - -struct OpenBaoAgentPaths { - stepca_agent_config: PathBuf, - responder_agent_config: PathBuf, - compose_override_path: Option, -} - async fn write_responder_files( secrets_dir: &Path, kv_mount: &str, @@ -609,13 +546,6 @@ template {{ config } -fn to_container_path(secrets_dir: &Path, path: &Path, messages: &Messages) -> Result { - let relative = path - .strip_prefix(secrets_dir) - .with_context(|| messages.error_resolve_path_failed(&path.display().to_string()))?; - Ok(format!("/openbao/secrets/{}", relative.to_string_lossy())) -} - async fn write_responder_compose_override( compose_file: &Path, secrets_dir: &Path, @@ -1156,38 +1086,6 @@ fn resolve_eab(args: &InitArgs, messages: &Messages) -> Result Result { - let compose_contents = std::fs::read_to_string(compose_file) - .with_context(|| messages.error_read_file_failed(&compose_file.display().to_string()))?; - Ok(compose_contents.contains("bootroot-http01")) -} - -fn compose_has_openbao(compose_file: &Path, messages: &Messages) -> Result { - let compose_contents = std::fs::read_to_string(compose_file) - .with_context(|| messages.error_read_file_failed(&compose_file.display().to_string()))?; - Ok(compose_contents.contains("openbao")) -} - -fn resolve_responder_url(args: &InitArgs, compose_has_responder: bool) -> Option { - if let Some(responder_url) = args.responder_url.as_ref() { - return Some(responder_url.clone()); - } - if compose_has_responder { - Some(DEFAULT_RESPONDER_ADMIN_URL.to_string()) - } else { - None - } -} - -fn resolve_openbao_agent_addr(openbao_url: &str, compose_has_openbao: bool) -> String { - if !compose_has_openbao { - return openbao_url.to_string(); - } - openbao_url - .replace("localhost", "openbao") - .replace("127.0.0.1", "openbao") -} - async fn verify_responder( responder_url: Option<&str>, args: &InitArgs, @@ -1730,8 +1628,9 @@ fn write_state_file( .collect(), apps: BTreeMap::new(), }; + let state_path = StateFile::default_path(); state - .save(Path::new("state.json")) + .save(&state_path) .with_context(|| messages.error_serialize_state_failed())?; Ok(()) } @@ -1796,64 +1695,6 @@ fn rollback_file(file: &RollbackFile, messages: &Messages) -> Result<()> { Ok(()) } -pub(crate) struct InitSummary { - pub(crate) openbao_url: String, - pub(crate) kv_mount: String, - pub(crate) secrets_dir: PathBuf, - pub(crate) show_secrets: bool, - pub(crate) init_response: bool, - pub(crate) root_token: String, - pub(crate) unseal_keys: Vec, - pub(crate) approles: Vec, - pub(crate) stepca_password: String, - pub(crate) db_dsn: String, - pub(crate) http_hmac: String, - pub(crate) eab: Option, - pub(crate) step_ca_result: StepCaInitResult, - pub(crate) responder_check: ResponderCheck, - pub(crate) responder_url: Option, - pub(crate) responder_template_path: PathBuf, - pub(crate) responder_config_path: PathBuf, - pub(crate) openbao_agent_stepca_config_path: PathBuf, - pub(crate) openbao_agent_responder_config_path: PathBuf, - pub(crate) openbao_agent_override_path: Option, - pub(crate) db_check: DbCheckStatus, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum DbCheckStatus { - Skipped, - Ok, -} - -pub(crate) struct InitPlan { - pub(crate) openbao_url: String, - pub(crate) kv_mount: String, - pub(crate) secrets_dir: PathBuf, - pub(crate) overwrite_password: bool, - pub(crate) overwrite_ca_json: bool, - pub(crate) overwrite_state: bool, -} - -pub(crate) struct AppRoleOutput { - pub(crate) label: String, - pub(crate) role_name: String, - pub(crate) role_id: String, - pub(crate) secret_id: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct EabCredentials { - pub(crate) kid: String, - pub(crate) hmac: String, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum StepCaInitResult { - Initialized, - Skipped, -} - #[cfg(test)] mod tests { use std::fs; @@ -1861,6 +1702,9 @@ mod tests { use tempfile::tempdir; + use super::super::constants::{ + DEFAULT_RESPONDER_ADMIN_URL, DEFAULT_STEPCA_PROVISIONER, DEFAULT_STEPCA_URL, + }; use super::*; use crate::i18n::Messages; diff --git a/src/commands/init/types.rs b/src/commands/init/types.rs new file mode 100644 index 0000000..554b6b0 --- /dev/null +++ b/src/commands/init/types.rs @@ -0,0 +1,65 @@ +use std::path::PathBuf; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum ResponderCheck { + Skipped, + Ok, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum DbCheckStatus { + Skipped, + Ok, +} + +#[derive(Debug, Clone)] +pub(crate) struct EabCredentials { + pub(crate) kid: String, + pub(crate) hmac: String, +} + +pub(crate) struct InitSummary { + pub(crate) openbao_url: String, + pub(crate) kv_mount: String, + pub(crate) secrets_dir: PathBuf, + pub(crate) show_secrets: bool, + pub(crate) init_response: bool, + pub(crate) root_token: String, + pub(crate) unseal_keys: Vec, + pub(crate) approles: Vec, + pub(crate) stepca_password: String, + pub(crate) db_dsn: String, + pub(crate) http_hmac: String, + pub(crate) eab: Option, + pub(crate) step_ca_result: StepCaInitResult, + pub(crate) responder_check: ResponderCheck, + pub(crate) responder_url: Option, + pub(crate) responder_template_path: PathBuf, + pub(crate) responder_config_path: PathBuf, + pub(crate) openbao_agent_stepca_config_path: PathBuf, + pub(crate) openbao_agent_responder_config_path: PathBuf, + pub(crate) openbao_agent_override_path: Option, + pub(crate) db_check: DbCheckStatus, +} + +pub(crate) struct InitPlan { + pub(crate) openbao_url: String, + pub(crate) kv_mount: String, + pub(crate) secrets_dir: PathBuf, + pub(crate) overwrite_password: bool, + pub(crate) overwrite_ca_json: bool, + pub(crate) overwrite_state: bool, +} + +pub(crate) struct AppRoleOutput { + pub(crate) label: String, + pub(crate) role_name: String, + pub(crate) role_id: String, + pub(crate) secret_id: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum StepCaInitResult { + Initialized, + Skipped, +} diff --git a/src/commands/rotate.rs b/src/commands/rotate.rs index bfe3077..d536a2f 100644 --- a/src/commands/rotate.rs +++ b/src/commands/rotate.rs @@ -20,32 +20,72 @@ use crate::commands::init::{ }; use crate::i18n::Messages; use crate::state::{AppEntry, StateFile}; - -const STATE_FILE_NAME: &str = "state.json"; -const STEPCA_ROOT_KEY: &str = "secrets/root_ca_key"; -const STEPCA_INTERMEDIATE_KEY: &str = "secrets/intermediate_ca_key"; -const STEPCA_PASSWORD_FILE: &str = "password.txt"; -const RESPONDER_CONFIG_PATH: &str = "responder/responder.toml"; -const CA_JSON_PATH: &str = "config/ca.json"; const SECRET_BYTES: usize = 32; const OPENBAO_AGENT_CONTAINER_PREFIX: &str = "bootroot-openbao-agent"; const ROLE_ID_FILENAME: &str = "role_id"; +#[derive(Debug, Clone)] +struct StatePaths { + state_file: PathBuf, + secrets_dir: PathBuf, +} + +impl StatePaths { + fn new(state_file: PathBuf, secrets_dir: PathBuf) -> Self { + Self { + state_file, + secrets_dir, + } + } + + fn state_file(&self) -> &Path { + &self.state_file + } + + fn secrets_dir(&self) -> &Path { + &self.secrets_dir + } + + fn stepca_password(&self) -> PathBuf { + self.secrets_dir.join("password.txt") + } + + fn stepca_password_new(&self) -> PathBuf { + self.secrets_dir.join("password.txt.new") + } + + fn stepca_root_key(&self) -> PathBuf { + self.secrets_dir.join("secrets").join("root_ca_key") + } + + fn stepca_intermediate_key(&self) -> PathBuf { + self.secrets_dir.join("secrets").join("intermediate_ca_key") + } + + fn responder_config(&self) -> PathBuf { + self.secrets_dir.join("responder").join("responder.toml") + } + + fn ca_json(&self) -> PathBuf { + self.secrets_dir.join("config").join("ca.json") + } +} + #[derive(Debug)] struct RotateContext { openbao_url: String, kv_mount: String, - secrets_dir: PathBuf, compose_file: PathBuf, root_token: String, state: StateFile, + paths: StatePaths, } pub(crate) async fn run_rotate(args: &RotateArgs, messages: &Messages) -> Result<()> { let state_path = args .state_file .clone() - .unwrap_or_else(|| PathBuf::from(STATE_FILE_NAME)); + .unwrap_or_else(StateFile::default_path); if !state_path.exists() { anyhow::bail!(messages.error_state_missing()); } @@ -67,15 +107,17 @@ pub(crate) async fn run_rotate(args: &RotateArgs, messages: &Messages) -> Result .secrets_dir .clone() .unwrap_or_else(|| state.secrets_dir()); + let paths = StatePaths::new(state_path, secrets_dir.clone()); let root_token = resolve_root_token(args.root_token.root_token.clone(), messages)?; let ctx = RotateContext { openbao_url, kv_mount, - secrets_dir, compose_file: args.compose.compose_file.clone(), root_token, state, + paths, }; + let _state_file = ctx.paths.state_file(); let mut client = OpenBaoClient::new(&ctx.openbao_url) .with_context(|| messages.error_openbao_client_create_failed())?; @@ -123,11 +165,11 @@ async fn rotate_stepca_password( messages, )?; - let secrets_dir = &ctx.secrets_dir; - let password_path = secrets_dir.join(STEPCA_PASSWORD_FILE); - let new_password_path = secrets_dir.join("password.txt.new"); - let root_key = secrets_dir.join(STEPCA_ROOT_KEY); - let intermediate_key = secrets_dir.join(STEPCA_INTERMEDIATE_KEY); + let secrets_dir = ctx.paths.secrets_dir(); + let password_path = ctx.paths.stepca_password(); + let new_password_path = ctx.paths.stepca_password_new(); + let root_key = ctx.paths.stepca_root_key(); + let intermediate_key = ctx.paths.stepca_intermediate_key(); ensure_file_exists(&password_path, messages)?; ensure_file_exists(&root_key, messages)?; @@ -233,7 +275,7 @@ async fn rotate_db( Some(value) => value, None => generate_secret(messages)?, }; - let ca_json_path = ctx.secrets_dir.join(CA_JSON_PATH); + let ca_json_path = ctx.paths.ca_json(); let current_dsn = read_ca_json_dsn(&ca_json_path, messages)?; let parsed = db::parse_db_dsn(¤t_dsn).with_context(|| messages.error_invalid_db_dsn())?; let timeout = Duration::from_secs(args.timeout.timeout_secs); @@ -301,7 +343,7 @@ async fn rotate_responder_hmac( .await .with_context(|| messages.error_openbao_kv_write_failed())?; - let responder_path = ctx.secrets_dir.join(RESPONDER_CONFIG_PATH); + let responder_path = ctx.paths.responder_config(); let config = if responder_path.exists() { let contents = fs::read_to_string(&responder_path).with_context(|| { messages.error_read_file_failed(&responder_path.display().to_string()) diff --git a/src/commands/verify.rs b/src/commands/verify.rs index 1b2867e..c3a182d 100644 --- a/src/commands/verify.rs +++ b/src/commands/verify.rs @@ -10,14 +10,12 @@ use crate::cli::prompt::Prompt; use crate::i18n::Messages; use crate::state::{AppEntry, DeployType, StateFile}; -const STATE_FILE_NAME: &str = "state.json"; - pub(crate) fn run_verify(args: &VerifyArgs, messages: &Messages) -> Result<()> { - let state_path = Path::new(STATE_FILE_NAME); + let state_path = StateFile::default_path(); if !state_path.exists() { anyhow::bail!(messages.error_state_missing()); } - let state = StateFile::load(state_path)?; + let state = StateFile::load(&state_path)?; let service_name = resolve_verify_service_name(args, messages)?; let entry = state .apps diff --git a/src/state.rs b/src/state.rs index 9dbb2be..505d898 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,6 +6,7 @@ use clap::ValueEnum; use serde::{Deserialize, Serialize}; const DEFAULT_SECRETS_DIR: &str = "secrets"; +const DEFAULT_STATE_FILE: &str = "state.json"; #[derive(Debug, Serialize, Deserialize)] pub(crate) struct StateFile { @@ -22,6 +23,10 @@ pub(crate) struct StateFile { } impl StateFile { + pub(crate) fn default_path() -> PathBuf { + PathBuf::from(DEFAULT_STATE_FILE) + } + pub(crate) fn load(path: &Path) -> Result { let contents = std::fs::read_to_string(path) .with_context(|| format!("Failed to read {}", path.display()))?;