282 lines
8.9 KiB
Rust
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(())
|
|
}
|
|
}
|
|
}
|