dotlock-bin 0.1.5

Encrypted project-local environment variables manager
use std::{process::Command, time::Instant};

use crate::{
    audit::{record_dynamic_resolve, record_run},
    domain::{error::DotLockError, model::DotLockResult},
    providers::resolve_provider,
    storage::{
        project::SECRETS_FILE,
        secrets_lock::{SecretKind, decrypt_secret_value, load_secrets_file},
        vault_file::load_vault_metadata,
    },
};

pub fn run_with_secrets(command: Vec<String>, dek: &[u8; 32]) -> DotLockResult<()> {
    if command.is_empty() {
        return Err(DotLockError::MissingCommand);
    }

    let file = load_secrets_file(SECRETS_FILE)?;

    let mut envs = Vec::new();

    for secret in &file.secrets {
        let value = match secret_value_for_runtime(&secret, dek, &file.secrets) {
            Ok(Some(value)) => value,
            Ok(None) => continue,
            Err(err) => return Err(err),
        };
        envs.push((secret.name.clone(), value));
    }

    let secrets_consumed = envs
        .iter()
        .map(|(name, _)| name.clone())
        .collect::<Vec<_>>();
    if let Err(err) = record_run(&command, &secrets_consumed) {
        eprintln!("warn: audit log write failed: {err}");
    }

    let program = &command[0];
    let args = &command[1..];

    let status = Command::new(program)
        .args(args)
        .envs(envs)
        .status()
        .map_err(|e| DotLockError::Io(e.to_string()))?;

    if !status.success() {
        return Err(DotLockError::CommandFailed {
            status: status.to_string(),
        });
    }

    Ok(())
}

pub fn secret_value_for_runtime(
    secret: &crate::storage::secrets_lock::SecretRecord,
    dek: &[u8; 32],
    all_secrets: &[crate::storage::secrets_lock::SecretRecord],
) -> DotLockResult<Option<String>> {
    match &secret.kind {
        SecretKind::Static => match decrypt_secret_value(secret, dek) {
            Ok(value) => Ok(Some(value)),
            Err(DotLockError::AccessDenied { .. }) => Ok(None),
            Err(err) => Err(err),
        },
        SecretKind::Dynamic {
            provider: _,
            config: _,
            bootstrap: _,
        } => {
            let dynamic = crate::storage::secrets_lock::decrypt_dynamic_metadata(secret, dek)?;
            let value = resolve_dynamic_secret(&secret.name, &dynamic, dek, all_secrets)?;
            Ok(Some(value))
        }
    }
}

pub fn resolve_dynamic_secret(
    secret_name: &str,
    dynamic: &crate::storage::secrets_lock::DynamicSecretMetadata,
    dek: &[u8; 32],
    all_secrets: &[crate::storage::secrets_lock::SecretRecord],
) -> DotLockResult<String> {
    let mut bootstrap_values = serde_json::Map::new();
    for bootstrap_name in &dynamic.bootstrap {
        let bootstrap_secret = all_secrets
            .iter()
            .find(|secret| &secret.name == bootstrap_name)
            .ok_or_else(|| DotLockError::SecretNotFound {
                name: bootstrap_name.clone(),
            })?;
        if !matches!(bootstrap_secret.kind, SecretKind::Static) {
            return Err(DotLockError::Io(format!(
                "bootstrap secret `{bootstrap_name}` must be static"
            )));
        }
        bootstrap_values.insert(
            bootstrap_name.clone(),
            serde_json::Value::String(decrypt_secret_value(bootstrap_secret, dek)?),
        );
    }

    let timeout = load_vault_metadata(crate::storage::project::VAULT_FILE)
        .map(|metadata| metadata.config.dynamic_resolve_timeout_secs.unwrap_or(10))
        .unwrap_or(10);
    let started = Instant::now();
    let provider_dir = dynamic
        .provider_path
        .as_deref()
        .and_then(|path| std::path::Path::new(path).parent());
    let resolved = resolve_provider(
        &dynamic.provider,
        &dynamic.config,
        &bootstrap_values,
        dynamic.provider_sha256.as_deref(),
        provider_dir,
        timeout,
    );
    let duration_ms = started.elapsed().as_millis();
    if let Err(err) = record_dynamic_resolve(
        &dynamic.provider,
        secret_name,
        duration_ms,
        resolved.is_ok(),
    ) {
        eprintln!("warn: audit log write failed: {err}");
    }
    resolved
}