secenv 0.2.0

Secure environments.
use {
    crate::{
        gcp::{
            GcpSecretManager,
            GcpSecretSpec,
        },
        gpg::{
            GpgKeySpec,
            GpgManager,
        },
    },
    anyhow::{
        Context,
        Result,
    },
    base64::Engine,
    semver::Version,
    serde::{
        Deserialize,
        Serialize,
    },
    std::collections::HashMap,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EncodedValue {
    Literal(String),
    Base64(String),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EncodedValueWrapper {
    #[serde(flatten)]
    pub inner: EncodedValue,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SecretAllocation {
    Literal(EncodedValue),
    File(String),
    Gpg { fingerprint: String },
    Gcp { secret: String, version: Option<String> },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SecretAllocationWrapper {
    #[serde(flatten)]
    pub inner: SecretAllocation,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Secret {
    #[serde(rename = "pgp")]
    PGP(SecretAllocationWrapper),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SecretWrapper {
    #[serde(flatten)]
    pub inner: Secret,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Manifest {
    pub version: String,
    #[serde(default)]
    pub profiles: HashMap<String, ManifestProfile>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ManifestProfile {
    #[serde(default)]
    pub files: HashMap<String, ContentWrapper>,

    #[serde(default)]
    pub env: ManifestEnv,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub struct ManifestEnv {
    #[serde(default)]
    pub keep: Option<Vec<String>>,

    #[serde(default)]
    pub vars: HashMap<String, ContentWrapper>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Content {
    Plain(EncodedValue),

    Secure {
        secret: SecretWrapper,
        value: EncodedValueWrapper,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ContentWrapper {
    #[serde(flatten)]
    pub inner: Content,
}

impl EncodedValue {
    pub fn get_value(&self) -> Result<String, anyhow::Error> {
        match self {
            | EncodedValue::Literal(value) => Ok(value.clone()),
            | EncodedValue::Base64(value) => {
                let decoded_bytes = base64::engine::general_purpose::STANDARD
                    .decode(value)
                    .context("Failed to decode base64 value")?;

                Ok(String::from_utf8(decoded_bytes).context("Decoded value is not valid UTF-8")?)
            },
        }
    }
}

impl SecretAllocation {
    pub fn get_value(&self) -> Result<String, anyhow::Error> {
        match self {
            | SecretAllocation::Literal(encoded_value) => encoded_value.get_value(),
            | SecretAllocation::File(file_path) => {
                std::fs::read_to_string(file_path).context(format!("Failed to read file: {}", file_path))
            },
            | SecretAllocation::Gpg { fingerprint } => {
                let gpg = GpgManager::new().context("Failed to initialize GPG manager")?;
                let spec = GpgKeySpec {
                    fingerprint: fingerprint.clone(),
                };
                gpg.export_private_key(&spec)
                    .context("Failed to export GPG private key")
            },
            | SecretAllocation::Gcp { secret, version } => {
                let gcp = GcpSecretManager::new().context("Failed to initialize GCP Secret Manager client")?;
                let spec = GcpSecretSpec {
                    secret: secret.clone(),
                    version: version.clone(),
                };
                gcp.access_secret(&spec).context("Failed to access GCP secret")
            },
        }
    }
}

impl Content {
    pub fn get_value(&self, pgp_manager: &mut crate::pgp::PgpManager) -> Result<String, anyhow::Error> {
        match self {
            | Content::Plain(encoded_value) => encoded_value.get_value(),
            | Content::Secure { secret, value } => {
                let encrypted_data = value.inner.get_value()?;

                match &secret.inner {
                    | Secret::PGP(allocation_wrapper) => {
                        match &allocation_wrapper.inner {
                            | SecretAllocation::Gpg { fingerprint: _ } => {
                                let gpg = GpgManager::new().context("Failed to initialize GPG manager")?;
                                gpg.decrypt_data(&encrypted_data)
                                    .context("Failed to decrypt value with GPG")
                            },
                            | _ => {
                                let pgp_key = allocation_wrapper.inner.get_value()?;
                                pgp_manager
                                    .decrypt(&pgp_key, &encrypted_data)
                                    .context("Failed to decrypt value with PGP key")
                            },
                        }
                    },
                }
            },
        }
    }
}

impl Manifest {
    pub fn validate_version(&self) -> Result<()> {
        let cli_version = Version::parse(env!("CARGO_PKG_VERSION")).context("Failed to parse CLI version")?;

        if env!("CARGO_PKG_VERSION") == "0.0.0" {
            return Ok(());
        }

        let config_version =
            Version::parse(&self.version).context(format!("Invalid version format in config: '{}'", self.version))?;

        if config_version.major != cli_version.major {
            return Err(anyhow::anyhow!(
                "Config version {} is incompatible with CLI version {}. Major version mismatch.",
                config_version,
                cli_version
            ));
        }

        if config_version > cli_version {
            return Err(anyhow::anyhow!(
                "Config version {} is newer than CLI version {}. Please upgrade the CLI.",
                config_version,
                cli_version
            ));
        }

        if config_version.minor > cli_version.minor {
            eprintln!(
                "Warning: Config version {} has newer minor version than CLI version {}. Some features may not work.",
                config_version, cli_version
            );
        }

        Ok(())
    }
}