smolvm_protocol/secrets.rs
1//! Secret reference types shared across smolvm surfaces.
2//!
3//! A [`SecretRef`] is a *pointer* to a secret. Refs travel across trust
4//! boundaries (HTTP request bodies, persisted VM records, `.smolmachine`
5//! pack manifests); resolved plaintext values do not.
6//!
7//! This crate carries only the *shape* of a ref — the on-the-wire and
8//! on-disk representation plus trivial introspection. The validation
9//! policy (which source kinds are allowed at which trust boundary)
10//! lives in the host crate alongside the code that enforces it. See
11//! `smolvm::secrets` for `ResolutionScope` and `validate_ref`.
12
13use serde::{Deserialize, Serialize};
14use std::path::PathBuf;
15
16/// Which source a [`SecretRef`] points at, independent of the data
17/// inside. Used by audit logging so the logger never sees the path or
18/// env-var name (which can themselves be revealing).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SecretSourceKind {
21 /// The ref points at an entry in the host secret store.
22 Store,
23 /// The ref points at a host environment variable.
24 Env,
25 /// The ref points at a host file path.
26 File,
27}
28
29impl SecretSourceKind {
30 /// Human-readable label.
31 pub fn as_str(self) -> &'static str {
32 match self {
33 Self::Store => "store",
34 Self::Env => "env",
35 Self::File => "file",
36 }
37 }
38}
39
40/// A reference to a secret. Exactly one of the three sources must be
41/// populated; validation is performed by the host crate's
42/// `validate_ref` (policy lives where it's enforced).
43///
44/// Round-trips through `serde_json` for persistence in the VM record DB
45/// and in `.smolmachine` pack manifests. Refs are not sensitive; the
46/// resolved plaintext is produced only at the workload launch site and
47/// never touches any of these stores.
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct SecretRef {
51 /// Look up the secret by name in the host secret store.
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub from_store: Option<String>,
54
55 /// Read the secret from a host environment variable.
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub from_env: Option<String>,
58
59 /// Read the secret from a host file path (must be absolute).
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub from_file: Option<PathBuf>,
62}
63
64impl SecretRef {
65 /// Return the source kind for this ref, if exactly one source is set.
66 ///
67 /// Returns `None` for structurally invalid refs (0 or >1 sources).
68 /// Callers are expected to have already validated with the host
69 /// crate's `validate_ref` before calling this; this function is
70 /// primarily for audit logging of a known-good ref.
71 pub fn source_kind(&self) -> Option<SecretSourceKind> {
72 match (
73 self.from_store.is_some(),
74 self.from_env.is_some(),
75 self.from_file.is_some(),
76 ) {
77 (true, false, false) => Some(SecretSourceKind::Store),
78 (false, true, false) => Some(SecretSourceKind::Env),
79 (false, false, true) => Some(SecretSourceKind::File),
80 _ => None,
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn source_kind_reports_variant() {
91 assert_eq!(
92 SecretRef {
93 from_store: Some("x".into()),
94 from_env: None,
95 from_file: None,
96 }
97 .source_kind(),
98 Some(SecretSourceKind::Store)
99 );
100 assert_eq!(
101 SecretRef {
102 from_store: None,
103 from_env: Some("Y".into()),
104 from_file: None,
105 }
106 .source_kind(),
107 Some(SecretSourceKind::Env)
108 );
109 assert_eq!(
110 SecretRef {
111 from_store: None,
112 from_env: None,
113 from_file: Some(PathBuf::from("/z")),
114 }
115 .source_kind(),
116 Some(SecretSourceKind::File)
117 );
118 let empty = SecretRef {
119 from_store: None,
120 from_env: None,
121 from_file: None,
122 };
123 assert_eq!(empty.source_kind(), None);
124 }
125
126 #[test]
127 fn deny_unknown_fields() {
128 let bad = r#"{ "from_stor": "typo" }"#;
129 let res: Result<SecretRef, _> = serde_json::from_str(bad);
130 assert!(res.is_err());
131 }
132
133 #[test]
134 fn roundtrip_json() {
135 let r = SecretRef {
136 from_store: Some("API_KEY".into()),
137 from_env: None,
138 from_file: None,
139 };
140 let json = serde_json::to_string(&r).unwrap();
141 let back: SecretRef = serde_json::from_str(&json).unwrap();
142 assert_eq!(r, back);
143 }
144
145 #[test]
146 fn serialization_omits_empty_fields() {
147 let r = SecretRef {
148 from_store: Some("X".into()),
149 from_env: None,
150 from_file: None,
151 };
152 let json = serde_json::to_string(&r).unwrap();
153 assert!(json.contains("from_store"));
154 assert!(!json.contains("from_env"));
155 assert!(!json.contains("from_file"));
156 }
157}