233 lines
7.9 KiB
Rust
233 lines
7.9 KiB
Rust
//! The token lifetime of the agents' cert-auth roles that already exist.
|
|
//!
|
|
//! A role is written with its lifetimes only when an agent's certificate is
|
|
//! minted or renewed, and a role written before the lifetimes were set leaves
|
|
//! every login at the mount's default of 32 days, one store lease each. The
|
|
//! start pass in [`crate::read_policy`] calls [`bring_current`] so those roles
|
|
//! take [`swarm_secret_client::client::CERT_ROLE_TOKEN_TTL`] without waiting for
|
|
//! a renewal.
|
|
//!
|
|
//! Only the two lifetimes are written: the role's certificate, allowed names
|
|
//! and policies stay as they are, and no token or lease is revoked. Tokens
|
|
//! issued before expire on their own.
|
|
|
|
use swarm_secret_client::{client::DEFAULT_CERT_MOUNT, policy};
|
|
|
|
use crate::store::AccessStore;
|
|
|
|
/// What one pass did, one count per role.
|
|
#[derive(Debug, Default, PartialEq, Eq)]
|
|
pub struct Counts {
|
|
pub rewritten: usize,
|
|
pub already_current: usize,
|
|
pub failed: usize,
|
|
}
|
|
|
|
/// Give every agent role among `roles`, a listing of the cert mount's roles,
|
|
/// the current token lifetimes, and log the counts once.
|
|
///
|
|
/// A role that cannot be read or written is named in a warning and skipped, so
|
|
/// it never stops the rest or the start. A role gone since the listing is
|
|
/// counted nowhere: there is nothing left to bring current.
|
|
pub async fn bring_current(store: &impl AccessStore, roles: &[String]) -> Counts {
|
|
let mut counts = Counts::default();
|
|
for agent in policy::agents_from_role_names(roles) {
|
|
let Ok(role) = policy::agent_object_name(&agent) else {
|
|
continue;
|
|
};
|
|
match bring_one(store, &role).await {
|
|
Ok(Outcome::Rewritten) => counts.rewritten += 1,
|
|
Ok(Outcome::AlreadyCurrent) => counts.already_current += 1,
|
|
Ok(Outcome::Gone) => {}
|
|
Err(e) => {
|
|
counts.failed += 1;
|
|
tracing::warn!(role, error = %e, "this agent role's token lifetime could not be brought current");
|
|
}
|
|
}
|
|
}
|
|
tracing::info!(
|
|
rewritten = counts.rewritten,
|
|
already_current = counts.already_current,
|
|
failed = counts.failed,
|
|
"agent role token lifetimes checked"
|
|
);
|
|
counts
|
|
}
|
|
|
|
enum Outcome {
|
|
Rewritten,
|
|
AlreadyCurrent,
|
|
Gone,
|
|
}
|
|
|
|
async fn bring_one(
|
|
store: &impl AccessStore,
|
|
role: &str,
|
|
) -> Result<Outcome, swarm_secret_client::Error> {
|
|
let Some(ttls) = store.cert_role_token_ttls(DEFAULT_CERT_MOUNT, role).await? else {
|
|
return Ok(Outcome::Gone);
|
|
};
|
|
if ttls.is_current() {
|
|
return Ok(Outcome::AlreadyCurrent);
|
|
}
|
|
store
|
|
.set_cert_role_token_ttls(DEFAULT_CERT_MOUNT, role)
|
|
.await?;
|
|
Ok(Outcome::Rewritten)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use swarm_secret_client::client::{CERT_ROLE_TOKEN_TTL_SECS, CertRoleTokenTtls};
|
|
|
|
use crate::store::fake::{FakeAccess, FakeRole};
|
|
|
|
use super::{Counts, bring_current};
|
|
|
|
const CURRENT: CertRoleTokenTtls = CertRoleTokenTtls {
|
|
token_ttl: CERT_ROLE_TOKEN_TTL_SECS,
|
|
token_max_ttl: CERT_ROLE_TOKEN_TTL_SECS,
|
|
};
|
|
const STALE: CertRoleTokenTtls = CertRoleTokenTtls {
|
|
token_ttl: 0,
|
|
token_max_ttl: 0,
|
|
};
|
|
|
|
fn role(agent: &str, ttls: CertRoleTokenTtls) -> FakeRole {
|
|
FakeRole {
|
|
certificate: format!("CA-{agent}"),
|
|
common_name: format!("hive-agent-{agent}"),
|
|
policies: vec!["hive-agent".to_owned()],
|
|
ttls,
|
|
}
|
|
}
|
|
|
|
fn listing(agents: &[&str]) -> Vec<String> {
|
|
agents.iter().map(|a| format!("hive-agent-{a}")).collect()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_stale_role_takes_the_ttl_and_keeps_its_certificate_names_and_policies() {
|
|
let before = role("a", STALE);
|
|
let store = FakeAccess::default().with_role("hive-agent-a", before.clone());
|
|
let counts = bring_current(&store, &listing(&["a"])).await;
|
|
assert_eq!(
|
|
counts,
|
|
Counts {
|
|
rewritten: 1,
|
|
..Counts::default()
|
|
}
|
|
);
|
|
assert_eq!(
|
|
store.role("hive-agent-a"),
|
|
Some(FakeRole {
|
|
ttls: CURRENT,
|
|
..before
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_role_with_a_lifetime_unset_or_different_is_rewritten() {
|
|
let store = FakeAccess::default()
|
|
.with_role(
|
|
"hive-agent-a",
|
|
role(
|
|
"a",
|
|
CertRoleTokenTtls {
|
|
token_ttl: CERT_ROLE_TOKEN_TTL_SECS,
|
|
token_max_ttl: 0,
|
|
},
|
|
),
|
|
)
|
|
.with_role(
|
|
"hive-agent-b",
|
|
role(
|
|
"b",
|
|
CertRoleTokenTtls {
|
|
token_ttl: 900,
|
|
token_max_ttl: 900,
|
|
},
|
|
),
|
|
);
|
|
let counts = bring_current(&store, &listing(&["a", "b"])).await;
|
|
assert_eq!(counts.rewritten, 2);
|
|
assert_eq!(store.role("hive-agent-a").expect("kept").ttls, CURRENT);
|
|
assert_eq!(store.role("hive-agent-b").expect("kept").ttls, CURRENT);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_current_role_is_read_and_not_written() {
|
|
let store = FakeAccess::default().with_role("hive-agent-a", role("a", CURRENT));
|
|
let counts = bring_current(&store, &listing(&["a"])).await;
|
|
assert_eq!(
|
|
counts,
|
|
Counts {
|
|
already_current: 1,
|
|
..Counts::default()
|
|
}
|
|
);
|
|
assert_eq!(store.calls(), ["cert_role_token_ttls hive-agent-a"]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_role_that_fails_does_not_stop_the_others() {
|
|
let store = FakeAccess::default()
|
|
.with_role("hive-agent-a", role("a", STALE))
|
|
.with_role("hive-agent-b", role("b", STALE))
|
|
.with_role("hive-agent-c", role("c", STALE))
|
|
.with_role("hive-agent-d", role("d", CURRENT))
|
|
.with_unreadable_role("hive-agent-a")
|
|
.with_unwritable_role("hive-agent-b");
|
|
let counts = bring_current(&store, &listing(&["a", "b", "c", "d"])).await;
|
|
assert_eq!(
|
|
counts,
|
|
Counts {
|
|
rewritten: 1,
|
|
already_current: 1,
|
|
failed: 2,
|
|
}
|
|
);
|
|
// The ones after the failures were still handled.
|
|
assert_eq!(store.role("hive-agent-c").expect("kept").ttls, CURRENT);
|
|
// The failed ones were left as they were.
|
|
assert_eq!(store.role("hive-agent-a").expect("kept").ttls, STALE);
|
|
assert_eq!(store.role("hive-agent-b").expect("kept").ttls, STALE);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn only_agent_roles_are_touched() {
|
|
let store = FakeAccess::default()
|
|
.with_role("hive-agent-a", role("a", STALE))
|
|
.with_role("hive-pr1ma", role("pr1ma", STALE))
|
|
.with_role("swarm-controller", role("c", STALE));
|
|
let roles = vec![
|
|
"hive-agent-a".to_owned(),
|
|
"hive-pr1ma".to_owned(),
|
|
"swarm-controller".to_owned(),
|
|
];
|
|
let counts = bring_current(&store, &roles).await;
|
|
assert_eq!(counts.rewritten, 1);
|
|
assert_eq!(store.role("hive-agent-a").expect("kept").ttls, CURRENT);
|
|
assert_eq!(store.role("hive-pr1ma").expect("kept").ttls, STALE);
|
|
assert_eq!(store.role("swarm-controller").expect("kept").ttls, STALE);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_role_gone_since_the_listing_is_counted_nowhere() {
|
|
let store = FakeAccess::default().with_role("hive-agent-b", role("b", STALE));
|
|
let counts = bring_current(&store, &listing(&["a", "b"])).await;
|
|
assert_eq!(
|
|
counts,
|
|
Counts {
|
|
rewritten: 1,
|
|
..Counts::default()
|
|
}
|
|
);
|
|
assert!(
|
|
!store
|
|
.calls()
|
|
.contains(&"set_cert_role_token_ttls hive-agent-a".to_owned())
|
|
);
|
|
}
|
|
}
|