//! The controller's own identity at the swarm's secret store. //! //! Two paths log in: writing an agent's credential, and writing the read //! grant that lets a hive fetch one back. Both present the same certificate //! under the same role, so the role is named here rather than at each caller. //! [`AccessStore`] is the subset of calls that give a principal its access, so //! a test can stand in for the store. use std::future::Future; use swarm_secret_client::{Error, SecretStore, client::EntityRef}; /// The cert-auth role the controller logs in under. /// /// `nix/host-modules/swarm-bao.nix`'s `controllerPolicyName` creates the /// role and names the policy after it. /// /// ⚠️ Not the certificate's CN. The role *matches on* the CN /// (`allowed_common_names`), so the two are deliberately different strings. pub const CERT_ROLE: &str = "swarm-controller"; /// Log in to the store with this deployment's certificate. /// /// # Errors /// Whatever [`SecretStore::from_env`] raises — an unset `BAO_*` variable, an /// unreadable identity file, or a store that refuses the login. pub async fn connect() -> Result { SecretStore::from_env(CERT_ROLE).await } /// The store calls that give a principal its access: policies, cert-auth roles /// and identity entities. A test stands in for the store through this trait. pub(crate) trait AccessStore { /// As [`SecretStore::write_policy`]. fn write_policy( &self, name: &str, policy: &str, ) -> impl Future> + Send; /// As [`SecretStore::write_cert_role`]. fn write_cert_role( &self, mount: &str, name: &str, certificate: &str, common_name: &str, policies: &[&str], ) -> impl Future> + Send; /// As [`SecretStore::auth_mount_accessor`]. fn auth_mount_accessor( &self, mount: &str, ) -> impl Future> + Send; /// As [`SecretStore::entity_by_alias`]. fn entity_by_alias( &self, alias_name: &str, mount_accessor: &str, ) -> impl Future, Error>> + Send; /// As [`SecretStore::entity_id_by_name`]. fn entity_id_by_name( &self, name: &str, ) -> impl Future, Error>> + Send; /// As [`SecretStore::create_entity`]. fn create_entity(&self, name: &str) -> impl Future> + Send; /// As [`SecretStore::rename_entity`]. fn rename_entity(&self, id: &str, name: &str) -> impl Future> + Send; /// As [`SecretStore::create_entity_alias`]. fn create_entity_alias( &self, alias_name: &str, mount_accessor: &str, entity_id: &str, ) -> impl Future> + Send; } impl AccessStore for SecretStore { async fn write_policy(&self, name: &str, policy: &str) -> Result<(), Error> { SecretStore::write_policy(self, name, policy).await } async fn write_cert_role( &self, mount: &str, name: &str, certificate: &str, common_name: &str, policies: &[&str], ) -> Result<(), Error> { SecretStore::write_cert_role(self, mount, name, certificate, common_name, policies).await } async fn auth_mount_accessor(&self, mount: &str) -> Result { SecretStore::auth_mount_accessor(self, mount).await } async fn entity_by_alias( &self, alias_name: &str, mount_accessor: &str, ) -> Result, Error> { SecretStore::entity_by_alias(self, alias_name, mount_accessor).await } async fn entity_id_by_name(&self, name: &str) -> Result, Error> { SecretStore::entity_id_by_name(self, name).await } async fn create_entity(&self, name: &str) -> Result<(), Error> { SecretStore::create_entity(self, name).await } async fn rename_entity(&self, id: &str, name: &str) -> Result<(), Error> { SecretStore::rename_entity(self, id, name).await } async fn create_entity_alias( &self, alias_name: &str, mount_accessor: &str, entity_id: &str, ) -> Result<(), Error> { SecretStore::create_entity_alias(self, alias_name, mount_accessor, entity_id).await } } /// A store that records what it is asked to do, for tests of the order and the /// outcome of the access calls. #[cfg(test)] pub(crate) mod fake { use std::sync::Mutex; use swarm_secret_client::{Error, client::EntityRef}; use super::AccessStore; /// What the fake answers; the default is a store with a readable cert mount /// and no entities, which accepts every write. #[derive(Default)] pub(crate) struct FakeAccess { /// The accessor read fails. no_accessor: bool, /// Every policy write is refused. refuse_policy: bool, /// The entity that owns each alias, as `(id, name)`. alias_owner: Option<(String, String)>, entities: Mutex>, calls: Mutex>, } impl FakeAccess { /// A store whose cert mount accessor cannot be read. pub(crate) fn without_accessor() -> Self { Self { no_accessor: true, ..Self::default() } } /// A store that refuses every policy write. pub(crate) fn refusing_policies() -> Self { Self { refuse_policy: true, ..Self::default() } } /// A store where the agent alias is already on the entity `name`. pub(crate) fn with_alias_on(id: &str, name: &str) -> Self { Self { alias_owner: Some((id.to_owned(), name.to_owned())), ..Self::default() } } /// The calls so far, as `verb name` strings, in order. pub(crate) fn calls(&self) -> Vec { self.calls.lock().expect("not poisoned").clone() } /// The `write_cert_role` calls so far, as `write_cert_role `. pub(crate) fn roles_written(&self) -> Vec { self.calls() .into_iter() .filter(|c| c.starts_with("write_cert_role ")) .collect() } fn log(&self, call: String) { self.calls.lock().expect("not poisoned").push(call); } fn denied() -> Error { Error::Settings("fake store: refused".to_owned()) } } impl AccessStore for FakeAccess { async fn write_policy(&self, name: &str, _policy: &str) -> Result<(), Error> { self.log(format!("write_policy {name}")); if self.refuse_policy { return Err(Self::denied()); } Ok(()) } async fn write_cert_role( &self, _mount: &str, name: &str, _certificate: &str, _common_name: &str, policies: &[&str], ) -> Result<(), Error> { self.log(format!("write_cert_role {name} {}", policies.join(","))); Ok(()) } async fn auth_mount_accessor(&self, _mount: &str) -> Result { self.log("auth_mount_accessor".to_owned()); if self.no_accessor { return Err(Self::denied()); } Ok("auth_cert_test".to_owned()) } async fn entity_by_alias( &self, alias_name: &str, _mount_accessor: &str, ) -> Result, Error> { self.log(format!("entity_by_alias {alias_name}")); Ok(self.alias_owner.as_ref().map(|(id, name)| EntityRef { id: id.clone(), name: name.clone(), })) } async fn entity_id_by_name(&self, name: &str) -> Result, Error> { self.log(format!("entity_id_by_name {name}")); let named = self .entities .lock() .expect("not poisoned") .contains(&name.to_owned()); Ok(named.then(|| format!("id-{name}"))) } async fn create_entity(&self, name: &str) -> Result<(), Error> { self.log(format!("create_entity {name}")); self.entities .lock() .expect("not poisoned") .push(name.to_owned()); Ok(()) } async fn rename_entity(&self, id: &str, name: &str) -> Result<(), Error> { self.log(format!("rename_entity {id} {name}")); Ok(()) } async fn create_entity_alias( &self, alias_name: &str, _mount_accessor: &str, _entity_id: &str, ) -> Result<(), Error> { self.log(format!("create_entity_alias {alias_name}")); Ok(()) } } }