Skip to main content

microsandbox_types/cloud/
secrets.rs

1//! Cloud secret-substitution wire contracts and domain conversions.
2
3use serde::{Deserialize, Serialize};
4use zeroize::Zeroizing;
5
6use crate::compat;
7use crate::domain::{
8    HostPattern, SecretEntry, SecretSubstitution, SecretViolationAction, SecretsConfig,
9};
10use crate::modify::SecretSource;
11
12//--------------------------------------------------------------------------------------------------
13// Types: Secrets
14//--------------------------------------------------------------------------------------------------
15
16/// Secret-substitution config for the cloud API. Twin of domain [`SecretsConfig`].
17#[derive(Debug, Clone, Default, Serialize)]
18#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
19#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
20pub struct CloudSecretsConfig {
21    /// Secrets to inject.
22    #[serde(default)]
23    pub entries: Vec<CloudSecretEntry>,
24    /// Default placeholder passthrough hosts, including for secrets added later.
25    /// A per-secret violation action overrides this default.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub passthrough_hosts: Option<Vec<CloudHostPattern>>,
28    /// Default action when a placeholder leaks to a disallowed host.
29    #[serde(default)]
30    pub violation_action: CloudViolationAction,
31}
32
33/// A single cloud secret entry. Twin of domain [`SecretEntry`].
34#[derive(Debug, Clone, Serialize)]
35#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
36#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
37pub struct CloudSecretEntry {
38    /// Environment variable name exposed to the sandbox.
39    pub env_var: String,
40    /// The secret value (empty when `source` carries a reference instead).
41    #[serde(default)]
42    pub value: String,
43    /// Host-side source resolved into `value` at spawn time.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub source: Option<CloudSecretSource>,
46    /// Placeholder the sandbox sees instead of the real value.
47    pub placeholder: String,
48    /// Hosts allowed to receive this secret.
49    #[serde(default)]
50    pub allowed_hosts: Vec<CloudHostPattern>,
51    /// Where the secret may be injected.
52    #[serde(default)]
53    pub substitution: SecretSubstitution,
54    /// Hosts allowed to receive the placeholder unchanged.
55    #[serde(default)]
56    pub passthrough_hosts: Vec<CloudHostPattern>,
57    /// Per-secret violation action overriding the config default.
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub violation_action: Option<CloudViolationAction>,
60    /// Require verified TLS identity before substituting (default: true).
61    #[serde(default)]
62    #[cfg_attr(feature = "utoipa", schema(default = true))]
63    pub require_tls_identity: bool,
64}
65
66/// Host-side source for a cloud secret. Twin of [`SecretSource`].
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
69#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
70#[serde(tag = "type", rename_all = "snake_case")]
71pub enum CloudSecretSource {
72    /// Read from a host environment variable at apply time.
73    Env {
74        /// Host environment variable name.
75        var: String,
76    },
77    /// Read from a host-side secret store reference.
78    Store {
79        /// Store-specific secret reference.
80        reference: String,
81    },
82}
83
84/// Host allowlist pattern for cloud secrets. Twin of [`HostPattern`], with the
85/// domain's scalar variants normalized to `{ value }` for a uniform union.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
88#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
89#[serde(tag = "type", rename_all = "snake_case")]
90pub enum CloudHostPattern {
91    /// Exact hostname match.
92    Exact {
93        /// Hostname to match exactly.
94        value: String,
95    },
96    /// Wildcard match (e.g. `*.openai.com`).
97    Wildcard {
98        /// Wildcard pattern.
99        value: String,
100    },
101    /// Any host (dangerous — the secret can be exfiltrated).
102    Any,
103}
104
105/// Action on a cloud secret violation. Twin of [`SecretViolationAction`].
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
108#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
109#[serde(tag = "type", rename_all = "snake_case")]
110pub enum CloudViolationAction {
111    /// Block the request silently.
112    Block,
113    /// Block and log (default).
114    #[default]
115    BlockAndLog,
116    /// Block and terminate the sandbox.
117    BlockAndTerminate,
118}
119
120//--------------------------------------------------------------------------------------------------
121// Trait Implementations
122//--------------------------------------------------------------------------------------------------
123
124impl<'de> Deserialize<'de> for CloudSecretsConfig {
125    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
126        compat::cloud::deserialize_secrets_config(deserializer)
127    }
128}
129
130impl<'de> Deserialize<'de> for CloudSecretEntry {
131    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
132        compat::cloud::deserialize_secret_entry(deserializer)
133    }
134}
135
136//--------------------------------------------------------------------------------------------------
137// Conversions: Secrets
138//--------------------------------------------------------------------------------------------------
139
140impl From<HostPattern> for CloudHostPattern {
141    fn from(pattern: HostPattern) -> Self {
142        match pattern {
143            HostPattern::Exact(value) => Self::Exact { value },
144            HostPattern::Wildcard(value) => Self::Wildcard { value },
145            HostPattern::Any => Self::Any,
146        }
147    }
148}
149
150impl From<CloudHostPattern> for HostPattern {
151    fn from(pattern: CloudHostPattern) -> Self {
152        match pattern {
153            CloudHostPattern::Exact { value } => Self::Exact(value),
154            CloudHostPattern::Wildcard { value } => Self::Wildcard(value),
155            CloudHostPattern::Any => Self::Any,
156        }
157    }
158}
159
160impl From<SecretViolationAction> for CloudViolationAction {
161    fn from(action: SecretViolationAction) -> Self {
162        match action {
163            SecretViolationAction::Block => Self::Block,
164            SecretViolationAction::BlockAndLog => Self::BlockAndLog,
165            SecretViolationAction::BlockAndTerminate => Self::BlockAndTerminate,
166        }
167    }
168}
169
170impl From<CloudViolationAction> for SecretViolationAction {
171    fn from(action: CloudViolationAction) -> Self {
172        match action {
173            CloudViolationAction::Block => Self::Block,
174            CloudViolationAction::BlockAndLog => Self::BlockAndLog,
175            CloudViolationAction::BlockAndTerminate => Self::BlockAndTerminate,
176        }
177    }
178}
179
180impl From<SecretSource> for CloudSecretSource {
181    fn from(source: SecretSource) -> Self {
182        match source {
183            SecretSource::Env { var } => Self::Env { var },
184            SecretSource::Store { reference } => Self::Store { reference },
185        }
186    }
187}
188
189impl From<CloudSecretSource> for SecretSource {
190    fn from(source: CloudSecretSource) -> Self {
191        match source {
192            CloudSecretSource::Env { var } => Self::Env { var },
193            CloudSecretSource::Store { reference } => Self::Store { reference },
194        }
195    }
196}
197
198impl From<SecretEntry> for CloudSecretEntry {
199    fn from(entry: SecretEntry) -> Self {
200        Self {
201            env_var: entry.env_var,
202            value: entry.value.to_string(),
203            source: entry.source.map(Into::into),
204            placeholder: entry.placeholder,
205            allowed_hosts: entry.allowed_hosts.into_iter().map(Into::into).collect(),
206            substitution: entry.substitution,
207            passthrough_hosts: entry
208                .passthrough_hosts
209                .into_iter()
210                .map(Into::into)
211                .collect(),
212            violation_action: entry.violation_action.map(Into::into),
213            require_tls_identity: entry.require_tls_identity,
214        }
215    }
216}
217
218impl From<CloudSecretEntry> for SecretEntry {
219    fn from(entry: CloudSecretEntry) -> Self {
220        Self {
221            env_var: entry.env_var,
222            value: Zeroizing::new(entry.value),
223            source: entry.source.map(Into::into),
224            placeholder: entry.placeholder,
225            allowed_hosts: entry.allowed_hosts.into_iter().map(Into::into).collect(),
226            substitution: entry.substitution,
227            passthrough_hosts: entry
228                .passthrough_hosts
229                .into_iter()
230                .map(Into::into)
231                .collect(),
232            violation_action: entry.violation_action.map(Into::into),
233            require_tls_identity: entry.require_tls_identity,
234        }
235    }
236}
237
238impl From<SecretsConfig> for CloudSecretsConfig {
239    fn from(config: SecretsConfig) -> Self {
240        Self {
241            entries: config.secrets.into_iter().map(Into::into).collect(),
242            passthrough_hosts: config
243                .passthrough_hosts
244                .map(|hosts| hosts.into_iter().map(Into::into).collect()),
245            violation_action: config.violation_action.into(),
246        }
247    }
248}
249
250impl From<CloudSecretsConfig> for SecretsConfig {
251    fn from(config: CloudSecretsConfig) -> Self {
252        Self {
253            secrets: config.entries.into_iter().map(Into::into).collect(),
254            passthrough_hosts: config
255                .passthrough_hosts
256                .map(|hosts| hosts.into_iter().map(Into::into).collect()),
257            violation_action: config.violation_action.into(),
258        }
259    }
260}