harn-vm 0.7.28

Async bytecode virtual machine for the Harn programming language
Documentation
use async_trait::async_trait;

use super::{
    emit_secret_access_event, RotationHandle, SecretBytes, SecretError, SecretId, SecretMeta,
    SecretProvider, SecretVersion,
};

#[derive(Debug, Clone)]
pub struct EnvSecretProvider {
    namespace: String,
}

impl EnvSecretProvider {
    pub fn new(namespace: impl Into<String>) -> Self {
        Self {
            namespace: namespace.into(),
        }
    }

    pub fn env_var_name(&self, id: &SecretId) -> String {
        let namespace = normalize_env_component(&id.namespace);
        let name = normalize_env_component(&id.name);
        match id.version {
            SecretVersion::Latest => format!("HARN_SECRET_{namespace}_{name}"),
            SecretVersion::Exact(version) => format!("HARN_SECRET_{namespace}_{name}_V{version}"),
        }
    }
}

#[async_trait]
impl SecretProvider for EnvSecretProvider {
    async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError> {
        let env_name = self.env_var_name(id);
        match std::env::var(&env_name) {
            Ok(value) if !value.is_empty() => {
                emit_secret_access_event("env", id);
                Ok(SecretBytes::from(value))
            }
            _ => Err(SecretError::NotFound {
                provider: "env".to_string(),
                id: id.clone(),
            }),
        }
    }

    async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError> {
        let env_name = self.env_var_name(id);
        let rendered = value.with_exposed(|bytes| {
            std::str::from_utf8(bytes)
                .map(|text| text.to_string())
                .map_err(|error| SecretError::Backend {
                    provider: "env".to_string(),
                    message: format!("env secrets must be valid UTF-8: {error}"),
                })
        })?;
        std::env::set_var(&env_name, rendered);
        Ok(())
    }

    async fn rotate(&self, _id: &SecretId) -> Result<RotationHandle, SecretError> {
        Err(SecretError::Unsupported {
            provider: "env".to_string(),
            operation: "rotate",
        })
    }

    async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
        let env_prefix = if prefix.name.is_empty() {
            format!(
                "HARN_SECRET_{}_",
                normalize_env_component(&prefix.namespace)
            )
        } else {
            self.env_var_name(prefix)
        };

        let items = std::env::vars()
            .filter_map(|(name, _)| {
                if !name.starts_with(&env_prefix) {
                    return None;
                }
                let suffix = name
                    .strip_prefix(&format!(
                        "HARN_SECRET_{}_",
                        normalize_env_component(&prefix.namespace)
                    ))
                    .unwrap_or_default()
                    .trim_start_matches('_')
                    .to_ascii_lowercase();
                Some(SecretMeta {
                    id: SecretId::new(prefix.namespace.clone(), suffix),
                    provider: "env".to_string(),
                })
            })
            .collect::<Vec<_>>();
        Ok(items)
    }

    fn namespace(&self) -> &str {
        &self.namespace
    }

    fn supports_versions(&self) -> bool {
        false
    }
}

fn normalize_env_component(value: &str) -> String {
    let mut normalized = String::with_capacity(value.len());
    let mut last_was_underscore = false;
    for ch in value.chars() {
        let mapped = if ch.is_ascii_alphanumeric() {
            ch.to_ascii_uppercase()
        } else {
            '_'
        };
        if mapped == '_' {
            if !last_was_underscore {
                normalized.push(mapped);
            }
            last_was_underscore = true;
        } else {
            normalized.push(mapped);
            last_was_underscore = false;
        }
    }

    normalized.trim_matches('_').to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn env_provider_uses_expected_variable_name() {
        let provider = EnvSecretProvider::new("harn/test");
        let id = SecretId::new("harn.orchestrator.github", "installation-12345/private-key");
        assert_eq!(
            provider.env_var_name(&id),
            "HARN_SECRET_HARN_ORCHESTRATOR_GITHUB_INSTALLATION_12345_PRIVATE_KEY"
        );
    }
}