swarm-secret-client: one module per kind of secret, not one struct
mara, reviewing the previous commit: "the field is specific to matrix, why add it to the general struct". She is right, and the answer is that there was no general struct — `Credential` had one consumer, the crate's only path builder was `matrix_account`, and `value` is pinned by `glue-matrix-bao-token.nix`, a matrix unit. It was matrix's throughout, wearing a general name; adding `homeserver` is what made that visible. `client` now moves whatever type a caller names and decodes nothing itself. That is forwarding rather than machinery: `vaultrs::kv2::read`/`set` are already generic over the payload. The matrix agreement moves to its own module holding both halves — where a credential lives (`account_path`, was `path::matrix_account`) and what the object at that path holds. `path` keeps only what every path obeys, so a second kind of swarm secret becomes a module beside `matrix` rather than another optional field on a struct it shares. argus raised the same collision from the other direction on #4092: two mutually-exclusive `Option`s modelling one concept is the failure mode this forecloses. `checked_segment` stays public in `path`: hive-priv builds an on-disk path from the same names and must accept the same charset. Behaviour is unchanged. The compatibility properties move with the struct — `Option` is what lets a pre-`homeserver` stored object decode, and `skip_serializing_if` is what keeps a token-only object free of `"homeserver":null` for that nix reader. Refs #3726
This commit is contained in:
parent
d1c0963fbd
commit
e263681f1d
6 changed files with 205 additions and 123 deletions
|
|
@ -1,8 +1,10 @@
|
|||
//! Where a credential lives, for both ends of the store.
|
||||
//! The rules every path into the store obeys, whatever kind of secret it
|
||||
//! addresses.
|
||||
//!
|
||||
//! The controller writes and a hive reads, and neither is senior to the other,
|
||||
//! so the path they must agree on is built here rather than formatted at each
|
||||
//! call site.
|
||||
//! so what they must agree on is stated once here rather than at each call
|
||||
//! site. A *particular* kind of secret builds its path from these pieces in its
|
||||
//! own module — [`crate::matrix`] is the one that exists.
|
||||
|
||||
use crate::Error;
|
||||
|
||||
|
|
@ -52,34 +54,15 @@ pub fn checked_segment(kind: &'static str, value: &str) -> Result<(), Error> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// The path holding `agent`'s token for the external matrix account `account`.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when either name contains anything but
|
||||
/// `[A-Za-z0-9_-]`, which is what keeps one agent's name from addressing
|
||||
/// another agent's secret.
|
||||
pub fn matrix_account(agent: &str, account: &str) -> Result<String, Error> {
|
||||
checked_segment("agent", agent)?;
|
||||
checked_segment("account", account)?;
|
||||
Ok(format!("{AGENT_PREFIX}/{agent}/matrix/{account}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_well_formed_pair_lands_under_the_agent_prefix() {
|
||||
let p = matrix_account("atlas", "ops-relay").expect("both segments are legal");
|
||||
assert_eq!(p, "swarm/agents/atlas/matrix/ops-relay");
|
||||
assert!(p.starts_with(AGENT_PREFIX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_segment_cannot_escape_its_own_directory() {
|
||||
// Each of these is a *different* way to address another agent's tree,
|
||||
// and the last two are the ones a charset check catches but a
|
||||
// `contains("..")` check does not.
|
||||
fn a_segment_that_could_change_a_paths_shape_is_refused() {
|
||||
// Each of these is a *different* way to reach outside the segment the
|
||||
// caller meant, and the last two are the ones a charset check catches
|
||||
// but a `contains("..")` check does not.
|
||||
for bad in [
|
||||
"../argus",
|
||||
"atlas/../argus",
|
||||
|
|
@ -89,12 +72,8 @@ mod tests {
|
|||
"",
|
||||
] {
|
||||
assert!(
|
||||
matrix_account(bad, "ops-relay").is_err(),
|
||||
"agent segment {bad:?} must be refused"
|
||||
);
|
||||
assert!(
|
||||
matrix_account("atlas", bad).is_err(),
|
||||
"account segment {bad:?} must be refused"
|
||||
checked_segment("agent", bad).is_err(),
|
||||
"segment {bad:?} must be refused"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -102,7 +81,21 @@ mod tests {
|
|||
#[test]
|
||||
fn the_legal_charset_is_actually_reachable() {
|
||||
// The control for the test above: if `checked_segment` rejected
|
||||
// everything, the escape cases would pass for the wrong reason.
|
||||
assert!(matrix_account("a-b_C9", "d-e_F0").is_ok());
|
||||
// everything, the escape cases would pass for the wrong reason. The
|
||||
// charset is the one `hive-priv` accepts for the same names on disk,
|
||||
// so uppercase and underscore have to stay legal here.
|
||||
assert!(checked_segment("agent", "a-b_C9").is_ok());
|
||||
assert!(checked_segment("account", "d-e_F0").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_rejected_segment_is_named_in_the_error() {
|
||||
// The caller usually got the name from config and needs to see which
|
||||
// of the two it was.
|
||||
let e = checked_segment("account", "a b").expect_err("a space is not legal");
|
||||
assert!(
|
||||
matches!(e, Error::PathSegment { kind, ref value } if kind == "account" && value == "a b"),
|
||||
"got {e:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue