name the agent's entity before narrowing its role, and test hive provisioning against a fake store
This commit is contained in:
parent
139007e656
commit
09d89507a9
1 changed files with 535 additions and 67 deletions
|
|
@ -3,8 +3,12 @@
|
|||
//! 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 swarm_secret_client::{Error, SecretStore};
|
||||
use std::future::Future;
|
||||
|
||||
use swarm_secret_client::{Error, SecretStore, client::EntityRef};
|
||||
|
||||
/// The cert-auth role the controller logs in under.
|
||||
///
|
||||
|
|
@ -23,3 +27,256 @@ pub const CERT_ROLE: &str = "swarm-controller";
|
|||
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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue