//! 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 { 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 { 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()) ); } }