fix(awssm,scaleway): a JSON null field is no value, not the string "null" - #358
Conversation
08c2fbc to
30171f4
Compare
|
we should share both codepaths with extract from file provider which supports JSON pointers |
|
Agreed, and there's a reason to do it beyond deduplication:
let selected = match selected {
serde_json::Value::String(value) => value.clone(),
value => serde_json::to_string(value).expect("serializing JSON value cannot fail"),
};Same shape as the two providers — string clones, everything else stringifies — so a pointer landing on a null yields So there are three copies of this, and folding the providers into the shared one without fixing it there would spread the defect rather than remove it. Two things I'd want your call on before I write it, because they aren't mechanical: 1. What a null means at each boundary. The providers return 2. Happy to take it either way — say which and I'll redo this PR as the shared implementation with the null case covered in one place. If you'd rather land the narrow fix first and unify separately, this one is self-contained and does not touch |
Three call sites select one value out of a JSON document and then render it: the awssm and scaleway providers, which take a flat `field` key, and Secrets::extract_stored_value, which takes a JSON Pointer. All three had grown the same match -- string clones, everything else stringifies -- so a JSON null became the four-character secret "null". On a provider that is wrong. A null carries no value, so the secret is not set there and the provider chain should continue; instead it satisfied a required secret and reached the program as a password spelled n-u-l-l. bw and dashlane already treat a null as absent. On an extract it is right, and deliberate: the pointer names one location and reports what the document holds there. test_json_extract_resolves_structured_values_after_decoding pins that end to end, so it is preserved. Rendering now lives in one place, crate::json_field, with the two policies named rather than left to coincide: render() for an extract pointer, and render_field() for a provider lookup, which returns None for a null. Selection stays with each caller, since a flat key and a JSON Pointer are not interchangeable -- a field literally named "a/b" would change meaning under pointer syntax. Tests cover both policies, that they agree on every non-null value, the provider null cases, and that an extract still renders a null. Dropping the null arm from render_field fails five of them.
30171f4 to
43f06ef
Compare
|
Done — pushed as the shared implementation. Rendering now lives in One thing the unification turned upThe two paths should differ on null, and one of them already said so:
assert_eq!(values["NULL_VALUE"].expose_secret(), "null");So rendering a null as So the module exposes the two policies by name rather than letting them coincide: render(&Value) -> SecretString // extract pointer: a null renders
render_field(&Value) -> Option<SecretString> // provider field: a null is absent
Result
Needs |
The bug
extract_json_keyreads one key out of a JSON secret value for aref'sfield. A string comes back as itself, a missing key asNone, and everything else falls through a catch-all rendering it withto_string().serde_jsonrendersValue::Nullas"null". So:against
{"username": "admin", "password": null}produces a secret whose value is the four charactersnull.Verified against the current code before changing anything:
That satisfies a required secret and reaches the program as a password spelled n-u-l-l. Nothing warns; as far as every later check is concerned the secret is present.
Why this is a bug and not a choice
The project already treats a JSON null as no value — in two other providers, deliberately:
bw.rs:538None | Some(serde_json::Value::Null) => Ok(None)dashlane.rs:236Value::Null => return None, under a doc comment reading "treating an absent or empty value as no value"awssm.rs"null"scaleway.rs"null"scaleway'sextract_json_keycarries the comment "mirroring the AWS provider'sfieldsemantics", so it inherited this rather than deciding it.(
keeper.rs:364also mentionsValue::Null => "null", but that isvalue_type, returning a type name for error messages — unrelated, and correct as it stands.)The change
One match arm in each provider. A null field now behaves exactly like an absent one. Numbers and bools keep rendering verbatim — only null moves.
Tests
awssm: null isNone; and a null field and a missing field agree with each otherscaleway: the null caseRemoving the new arm fails all three, while the five pre-existing
extract_json_keytests still pass — so they pin the behaviour rather than the implementation.Full suite: 1279 passed, 0 failed (baseline before the change was 1276). Getting there needed
jq,sops≥ 3.13 andageon PATH — withoutjq66bwtests fail on a shim, and an oldersopsfails 14 more onflag provided but not defined: -value-stdin. Might be worth a line in the contributing notes; happy to send that separately.One caveat in the interest of accuracy: the suite has a flaky test under parallel execution —
provider::passbolt::tests::uuid_writes_bypass_the_folder_listingfailed once in about six runs, on a clean tree as well as with this change, and passes 3/3 in isolation. Unrelated to this patch, but you may want to know.