Skip to main content

greentic_runner_host/
secrets.rs

1use std::sync::Arc;
2
3use crate::runtime::block_on;
4use anyhow::{Result, anyhow};
5use greentic_secrets_lib::env::EnvSecretsManager;
6use greentic_secrets_lib::{SecretScope, SecretsManager};
7use greentic_types::TenantCtx;
8
9/// Shared secrets manager handle used by the host.
10pub type DynSecretsManager = Arc<dyn SecretsManager>;
11
12/// Supported secrets backend kinds recognised by the runner.
13#[derive(Clone, Debug)]
14pub enum SecretsBackend {
15    Env,
16}
17
18impl SecretsBackend {
19    pub fn from_env(value: Option<String>) -> Result<Self> {
20        match value
21            .unwrap_or_else(|| "env".into())
22            .trim()
23            .to_ascii_lowercase()
24            .as_str()
25        {
26            "" | "env" => Ok(SecretsBackend::Env),
27            other => Err(anyhow!("unsupported SECRETS_BACKEND `{other}`")),
28        }
29    }
30
31    pub fn from_config(cfg: &greentic_config_types::SecretsBackendRefConfig) -> Result<Self> {
32        match cfg.kind.trim().to_ascii_lowercase().as_str() {
33            "" | "none" | "env" => Ok(SecretsBackend::Env),
34            other => Err(anyhow!("unsupported secrets backend `{other}`")),
35        }
36    }
37
38    pub fn build_manager(&self) -> Result<DynSecretsManager> {
39        match self {
40            SecretsBackend::Env => {
41                ensure_env_secrets_allowed()?;
42                Ok(Arc::new(EnvSecretsManager) as DynSecretsManager)
43            }
44        }
45    }
46}
47
48pub fn default_manager() -> Result<DynSecretsManager> {
49    SecretsBackend::Env.build_manager()
50}
51
52pub fn scoped_secret_path(ctx: &TenantCtx, key: &str) -> Result<String> {
53    let key = key.trim();
54    if key.is_empty() {
55        return Err(anyhow!("secret key must not be empty"));
56    }
57    let safe_key = key.replace('/', ".").replace(' ', "_");
58    let user = ctx.user_id.as_ref().or(ctx.user.as_ref());
59    let name = if let Some(user_id) = user {
60        format!("user.{}.{}", user_id.as_str(), safe_key)
61    } else {
62        safe_key
63    };
64    let team = ctx.team_id.as_ref().or(ctx.team.as_ref());
65    let scope = SecretScope {
66        env: ctx.env.as_str().to_string(),
67        tenant: ctx.tenant.as_str().to_string(),
68        team: team.map(|value| value.as_str().to_string()),
69    };
70    let team_segment = scope.team.as_deref().unwrap_or("_");
71    Ok(format!(
72        "secrets://{}/{}/{}/kv/{}",
73        scope.env, scope.tenant, team_segment, name
74    ))
75}
76
77pub fn read_secret_blocking(
78    manager: &DynSecretsManager,
79    ctx: &TenantCtx,
80    key: &str,
81) -> Result<Vec<u8>> {
82    let scoped_key = scoped_secret_path(ctx, key)?;
83    let bytes =
84        block_on(manager.read(scoped_key.as_str())).map_err(|err| anyhow!(err.to_string()))?;
85    Ok(bytes)
86}
87
88pub fn write_secret_blocking(
89    manager: &DynSecretsManager,
90    ctx: &TenantCtx,
91    key: &str,
92    value: &[u8],
93) -> Result<()> {
94    let scoped_key = scoped_secret_path(ctx, key)?;
95    block_on(manager.write(scoped_key.as_str(), value)).map_err(|err| anyhow!(err.to_string()))?;
96    Ok(())
97}
98
99fn ensure_env_secrets_allowed() -> Result<()> {
100    let env = std::env::var("GREENTIC_ENV").unwrap_or_else(|_| "local".to_string());
101    let env = env.trim().to_ascii_lowercase();
102    if matches!(env.as_str(), "local" | "dev" | "test") {
103        Ok(())
104    } else {
105        Err(anyhow!(
106            "env secrets backend is disabled for env '{env}' (dev/test only)"
107        ))
108    }
109}