Skip to content

Commit 34526e7

Browse files
committed
Fix legacy discovery_type deserialization
Add custom Deserialize impl for ResolvableSecret that accepts both the current tagged-enum format and pre-v0.15.0 plain strings stored in discovery_type JSONB. Prevents deserialization failure on upgrade from instances with existing Network discovery records.
1 parent 236ab5e commit 34526e7

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

backend/src/server/credentials/impl/mapping.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,47 @@ pub enum ResolvableValue {
183183

184184
/// Secret value — inline or file path. Daemon wraps resolved value in Secret<String>.
185185
/// Never logged in plaintext.
186-
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema)]
186+
///
187+
/// Custom Deserialize accepts both the current tagged-enum format
188+
/// (`{"mode":"Value","value":"..."}`) and legacy plain strings (`"********"`)
189+
/// from pre-v0.15.0 discovery_type JSONB. Legacy strings deserialize as
190+
/// `Value { value: string }`.
191+
#[derive(Debug, Clone, Serialize, Eq, PartialEq, Hash, ToSchema)]
187192
#[serde(tag = "mode")]
188193
pub enum ResolvableSecret {
189194
Value { value: String },
190195
FilePath { path: String },
191196
}
192197

198+
impl<'de> Deserialize<'de> for ResolvableSecret {
199+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
200+
where
201+
D: serde::Deserializer<'de>,
202+
{
203+
let value = serde_json::Value::deserialize(deserializer)?;
204+
match &value {
205+
serde_json::Value::String(s) => Ok(ResolvableSecret::Value { value: s.clone() }),
206+
serde_json::Value::Object(_) => {
207+
#[derive(Deserialize)]
208+
#[serde(tag = "mode")]
209+
enum Tagged {
210+
Value { value: String },
211+
FilePath { path: String },
212+
}
213+
let tagged: Tagged =
214+
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
215+
Ok(match tagged {
216+
Tagged::Value { value } => ResolvableSecret::Value { value },
217+
Tagged::FilePath { path } => ResolvableSecret::FilePath { path },
218+
})
219+
}
220+
_ => Err(serde::de::Error::custom(
221+
"expected string or object for ResolvableSecret",
222+
)),
223+
}
224+
}
225+
}
226+
193227
impl ResolvableValue {
194228
/// Resolve to a string value. FilePath variant reads from disk.
195229
pub fn resolve(&self, field_name: &str, label: &str) -> Result<String, anyhow::Error> {

backend/src/server/credentials/impl/types/snmp.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,21 @@ mod tests {
274274
assert_eq!(community_value(&roundtripped), "my-secret");
275275
}
276276

277+
#[test]
278+
fn legacy_string_community_deserializes() {
279+
// Pre-v0.15.0 discovery_type JSONB stored community as a plain string
280+
// (redacted via Secret<String> serialize). The custom ResolvableSecret
281+
// deserializer must accept this format.
282+
let json = r#"{"version":"V2c","community":"********"}"#;
283+
let cred: SnmpQueryCredential = serde_json::from_str(json).unwrap();
284+
assert_eq!(
285+
cred.community,
286+
ResolvableSecret::Value {
287+
value: "********".to_string()
288+
}
289+
);
290+
}
291+
277292
#[test]
278293
fn specificity_ordering() {
279294
let ip: IpAddr = "10.0.0.1".parse().unwrap();

0 commit comments

Comments
 (0)