add one shared templated hive policy keyed on the cert alias name
This commit is contained in:
parent
e437f817dd
commit
03796f1964
4 changed files with 232 additions and 12 deletions
|
|
@ -151,15 +151,49 @@ fn list_stanza(path: &str) -> String {
|
|||
/// stanza could grant itself anything.
|
||||
pub fn render(hive: &str) -> Result<String, Error> {
|
||||
checked_segment("hive", hive)?;
|
||||
Ok(hive_document(hive))
|
||||
}
|
||||
|
||||
/// The text of [`render`] with `own` as the hive segment: a checked hive name,
|
||||
/// or the template [`render_shared_hives`] puts there.
|
||||
fn hive_document(own: &str) -> String {
|
||||
let agents = read_stanza(&format!(
|
||||
"{MOUNT}/data/{ROOT}/{}/*",
|
||||
<&str>::from(Kind::Agent)
|
||||
));
|
||||
let own = read_stanza(&format!(
|
||||
"{MOUNT}/data/{ROOT}/{}/{hive}/*",
|
||||
"{MOUNT}/data/{ROOT}/{}/{own}/*",
|
||||
<&str>::from(Kind::Hive)
|
||||
));
|
||||
Ok(format!("{agents}{own}"))
|
||||
format!("{agents}{own}")
|
||||
}
|
||||
|
||||
/// The one policy every hive's cert-auth role attaches next to its own.
|
||||
///
|
||||
/// Inside [`HIVE_PREFIX`] so that the controller may write it. It is the object
|
||||
/// name of a hive called `hives`, and `nix/reserved-hive-fragments.nix` forbids
|
||||
/// the substring `hive` in every hive name, so no hive spells it.
|
||||
pub const SHARED_HIVE_POLICY: &str = "hive-hives";
|
||||
|
||||
/// Render the policy [`SHARED_HIVE_POLICY`] holds: the text of [`render`], with
|
||||
/// the hive's name taken from the alias of its certificate login on the cert
|
||||
/// auth mount `mount_accessor`.
|
||||
///
|
||||
/// A hive's certificate carries its bare name as the common name, and that is
|
||||
/// the alias name, so the own-path stanza reads `swarm/hives/<hive>/` for
|
||||
/// whichever hive presents the token. A login whose alias is not a hive's name
|
||||
/// reads a path where nothing is stored. The `agents/` and `hives/` segments
|
||||
/// are fixed text, never taken from the identity.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `mount_accessor` holds anything but
|
||||
/// `[A-Za-z0-9_-]`: it is interpolated into the policy text, where it could
|
||||
/// close the stanza.
|
||||
pub fn render_shared_hives(mount_accessor: &str) -> Result<String, Error> {
|
||||
checked_segment("mount accessor", mount_accessor)?;
|
||||
Ok(hive_document(&format!(
|
||||
"{{{{identity.entity.aliases.{mount_accessor}.name}}}}"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Render `agent`'s policy document: read on that one agent's credentials, list
|
||||
|
|
@ -286,6 +320,68 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shared_hive_document_is_each_hives_own_with_the_alias_name_for_the_hive() {
|
||||
let shared = render_shared_hives("auth_cert_1a2b3c4d").expect("legal accessor");
|
||||
assert_eq!(
|
||||
shared,
|
||||
"path \"secret/data/swarm/agents/*\" {\n capabilities = [\"read\"]\n}\n\
|
||||
path \"secret/data/swarm/hives/{{identity.entity.aliases.auth_cert_1a2b3c4d.name}}/*\" {\n capabilities = [\"read\"]\n}\n"
|
||||
);
|
||||
// Same text as every hive's own document once the template is filled
|
||||
// in, for hives that differ in length, case and punctuation.
|
||||
for hive in ["pr1ma", "a", "Mixed_Case-1", "x".repeat(63).as_str()] {
|
||||
assert_eq!(
|
||||
shared.replace("{{identity.entity.aliases.auth_cert_1a2b3c4d.name}}", hive),
|
||||
render(hive).expect("legal"),
|
||||
"{hive}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shared_hive_document_names_no_hive_and_grants_no_wider_than_one() {
|
||||
let shared = render_shared_hives("auth_cert_1a2b3c4d").expect("legal accessor");
|
||||
assert!(!shared.contains("pr1ma"));
|
||||
assert!(!shared.contains("hives/*"), "{shared}");
|
||||
assert!(!shared.contains("create"));
|
||||
assert!(!shared.contains("update"));
|
||||
assert!(!shared.contains("delete"));
|
||||
assert!(!shared.contains("list"));
|
||||
// The control: the walk finds the stanzas that are there.
|
||||
assert_eq!(stanza_paths(&shared).len(), 2, "{shared}");
|
||||
assert!(
|
||||
stanza_paths(&shared)
|
||||
.iter()
|
||||
.any(|g| g.starts_with("swarm/hives/{{"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_accessor_that_could_close_the_stanza_is_refused() {
|
||||
for bad in ["", "a.b", "a}}b", "a\"b", "a/b", "a b"] {
|
||||
assert!(
|
||||
matches!(render_shared_hives(bad), Err(Error::PathSegment { .. })),
|
||||
"{bad:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shared_hive_policy_name_is_writable_and_no_agents() {
|
||||
assert!(SHARED_HIVE_POLICY.starts_with(HIVE_PREFIX));
|
||||
assert!(
|
||||
agents_from_role_names(&[SHARED_HIVE_POLICY.to_owned()]).is_empty(),
|
||||
"it must not be read back as an agent"
|
||||
);
|
||||
assert_ne!(SHARED_HIVE_POLICY, SHARED_AGENT_POLICY);
|
||||
// The only hive name that would render it carries `hive`.
|
||||
assert_eq!(
|
||||
hive_object_name("hives").expect("legal as a segment"),
|
||||
SHARED_HIVE_POLICY
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hive_reaches_no_swarm_services_secret() {
|
||||
// A swarm service's OIDC client secret is read under a certificate that
|
||||
|
|
|
|||
Loading…
Reference in a new issue