Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-controller/src/store.rs

282 lines
8.9 KiB
Rust

//! 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, Error> {
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<Output = Result<(), Error>> + Send;
/// As [`SecretStore::write_cert_role`].
fn write_cert_role(
&self,
mount: &str,
name: &str,
certificate: &str,
common_name: &str,
policies: &[&str],
) -> impl Future<Output = Result<(), Error>> + Send;
/// As [`SecretStore::auth_mount_accessor`].
fn auth_mount_accessor(
&self,
mount: &str,
) -> impl Future<Output = Result<String, Error>> + Send;
/// As [`SecretStore::entity_by_alias`].
fn entity_by_alias(
&self,
alias_name: &str,
mount_accessor: &str,
) -> impl Future<Output = Result<Option<EntityRef>, Error>> + Send;
/// As [`SecretStore::entity_id_by_name`].
fn entity_id_by_name(
&self,
name: &str,
) -> impl Future<Output = Result<Option<String>, Error>> + Send;
/// As [`SecretStore::create_entity`].
fn create_entity(&self, name: &str) -> impl Future<Output = Result<(), Error>> + Send;
/// As [`SecretStore::rename_entity`].
fn rename_entity(&self, id: &str, name: &str)
-> impl Future<Output = Result<(), Error>> + Send;
/// As [`SecretStore::create_entity_alias`].
fn create_entity_alias(
&self,
alias_name: &str,
mount_accessor: &str,
entity_id: &str,
) -> impl Future<Output = Result<(), Error>> + 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<String, Error> {
SecretStore::auth_mount_accessor(self, mount).await
}
async fn entity_by_alias(
&self,
alias_name: &str,
mount_accessor: &str,
) -> Result<Option<EntityRef>, Error> {
SecretStore::entity_by_alias(self, alias_name, mount_accessor).await
}
async fn entity_id_by_name(&self, name: &str) -> Result<Option<String>, 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<Vec<String>>,
calls: Mutex<Vec<String>>,
}
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<String> {
self.calls.lock().expect("not poisoned").clone()
}
/// The `write_cert_role` calls so far, as `write_cert_role <role> <policies>`.
pub(crate) fn roles_written(&self) -> Vec<String> {
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<String, Error> {
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<Option<EntityRef>, 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<Option<String>, 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(())
}
}
}