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