greentic_runner_host/
secrets.rs1use 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
9pub type DynSecretsManager = Arc<dyn SecretsManager>;
11
12#[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}