Watch
0
0
Fork
You've already forked hyperhive
0

name the agent's entity before narrowing its role, and test hive provisioning against a fake store

This commit is contained in:
atlas 2026-10-27 10:11:02 +01:00
commit 09d89507a9
5 changed files with 535 additions and 67 deletions

View file

@ -377,12 +377,15 @@ is one write. `swarm-controller` writes it at agent creation and at its own
start.
For that the controller names each agent's identity entity after the agent: at
agent creation, and for every agent at its own start, it reads the cert mount's
accessor and creates the entity `<agent>` with the alias `hive-agent-<agent>`,
or renames the entity a first login created under a generated name. When the alias
sits on any other entity, the controller logs an error and changes nothing, and
that agent reads nothing until an operator resolves it. It needs the `identity/`
and `sys/auth/cert` grants in `swarm-bao.nix`'s `controllerPolicyText` for that.
each certificate mint or renewal, before it writes the agent's cert-auth role,
and for every agent at its own start. It reads the cert mount's accessor and
creates the entity `<agent>` with the alias `hive-agent-<agent>`, or renames the
entity a first login created under a generated name. At a mint or renewal this is
a precondition: when the store refuses a call, or the alias sits on any other
entity, the mint fails and the role stays as it was, so a live agent keeps
working until an operator resolves it. The start pass only logs the same
failures, because it writes no role. The controller needs the `identity/` and
`sys/auth/cert` grants in `swarm-bao.nix`'s `controllerPolicyText` for that.
A cert-auth role that predates the shared policy may still name a policy
`hive-agent-<agent>`. The controller no longer writes or attaches these. The

View file

@ -13,20 +13,22 @@
//! is ever moved. Any other arrangement is a [`Plan::Conflict`]: nothing is
//! written and an operator decides.
//!
//! Everything else here is best effort: a failure is logged at `warn` and the
//! caller goes on. An agent whose entity is not named after it reads nothing,
//! because the shared policy is the only one its role attaches; at mint the
//! read-back fails, and on a start pass its next reads are refused.
//! An agent whose entity is not named after it reads nothing under the shared
//! policy, so what a failure costs depends on the caller. [`ensure_agent`],
//! which runs before a cert-auth role is narrowed to that policy, returns the
//! error and leaves the role as it was. [`ensure_agents`], the pass at every
//! start, writes no role and so narrows nothing: it logs at `warn` and goes on.
//! The grants are the `identity/` stanzas of `swarm-bao.nix`'s
//! `controllerPolicyText`.
use anyhow::{Context, Result, bail};
use swarm_secret_client::{
SecretStore,
client::{DEFAULT_CERT_MOUNT, EntityRef},
policy,
};
use crate::store::AccessStore;
/// What the store holds for one agent's entity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Seen {
@ -92,13 +94,13 @@ pub fn plan(agent: &str, seen: &Seen) -> Plan {
/// Make sure every agent in `agents` has its entity, logging what could not be
/// done. Never fails: see the module header.
pub async fn ensure_agents(store: &SecretStore, agents: &[String]) {
pub async fn ensure_agents(store: &impl AccessStore, agents: &[String]) {
let accessor = match store.auth_mount_accessor(DEFAULT_CERT_MOUNT).await {
Ok(accessor) => accessor,
Err(e) => {
tracing::warn!(
error = %e,
"agent entities are not managed: the cert auth mount's accessor could not be read; an agent whose entity is not named after it cannot read its secrets"
"agent entities are not managed: the cert auth mount's accessor could not be read; an agent whose role attaches the shared policy alone reads nothing until its entity is named after it"
);
return;
}
@ -120,13 +122,30 @@ pub async fn ensure_agents(store: &SecretStore, agents: &[String]) {
);
}
/// [`ensure_agents`] for one agent.
pub async fn ensure_agent(store: &SecretStore, agent: &str) {
ensure_agents(store, &[agent.to_owned()]).await;
/// Make sure `agent` has its entity, or say why not.
///
/// For a caller that must not go on without it, such as the one that narrows a
/// cert-auth role to the shared policy: an agent whose entity is not named after
/// it reads nothing under that policy.
///
/// # Errors
/// When the cert mount's accessor cannot be read, when a store call fails, and
/// on a [`Plan::Conflict`], which [`ensure_agents`] only logs.
pub async fn ensure_agent(store: &impl AccessStore, agent: &str) -> Result<()> {
let accessor = store
.auth_mount_accessor(DEFAULT_CERT_MOUNT)
.await
.context("reading the cert auth mount's accessor")?;
match ensure_one(store, &accessor, agent).await? {
Plan::Conflict { alias, entity_name } => bail!(
"the alias {alias} is on the entity {entity_name}, which is not {agent}; nothing was changed"
),
_ => Ok(()),
}
}
/// Observe, plan and apply for one agent.
async fn ensure_one(store: &SecretStore, accessor: &str, agent: &str) -> Result<Plan> {
async fn ensure_one(store: &impl AccessStore, accessor: &str, agent: &str) -> Result<Plan> {
let alias = policy::agent_object_name(agent)?;
let seen = Seen {
by_alias: store
@ -174,7 +193,7 @@ async fn ensure_one(store: &SecretStore, accessor: &str, agent: &str) -> Result<
agent,
alias,
entity_name,
"the agent's alias is on an entity that is not the agent's; nothing was changed and the agent cannot read its secrets until an operator resolves it"
"the agent's alias is on an entity that is not the agent's; nothing was changed; under the shared policy alone the agent reads nothing until an operator resolves it"
);
}
}

View file

@ -29,6 +29,7 @@
//! [`crate::agent_renewal`] re-issues a live agent's leaf once it is past half
//! its validity (the role's `ttl`), by queueing the same node as creation.
use crate::store::AccessStore;
use anyhow::{Context, Result, bail};
use swarm_secret_client::{
SecretStore,
@ -79,6 +80,48 @@ fn role_inputs<'a>(agent: &str, credential: &'a mtls::Credential) -> Result<Role
})
}
/// Write the shared policy, name the agent's entity, then write the cert-auth
/// role that attaches the policy.
///
/// The entity comes before the role and is a precondition for it. The role
/// attaches the shared policy alone, so an agent whose entity is not named after
/// it reads nothing under it. This also runs for a live agent's renewal, which
/// rewrites the role its current leaf logs in under: when the entity step
/// fails, or finds the alias on another entity, the role is left as it was and
/// the renewal fails while the agent keeps working.
async fn publish_access(
store: &impl AccessStore,
agent: &str,
inputs: &RoleInputs<'_>,
) -> Result<()> {
store
.write_policy(policy::SHARED_AGENT_POLICY, &policy::render_shared_agent())
.await
.with_context(|| {
format!(
"writing the shared agent policy {}",
policy::SHARED_AGENT_POLICY
)
})?;
crate::agent_entity::ensure_agent(store, agent)
.await
.with_context(|| format!("naming {agent}'s identity entity; the role is not changed"))?;
store
.write_cert_role(
DEFAULT_CERT_MOUNT,
&inputs.name,
inputs.ca,
&inputs.name,
&inputs
.attached
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
)
.await
.with_context(|| format!("writing the cert-auth role {}", inputs.name))
}
/// How many bytes of kernel randomness a queue secret is before encoding.
///
/// Thirty-two because the secret is a bearer token compared for equality and
@ -121,12 +164,12 @@ pub(crate) fn generate_queue_secret() -> Result<String> {
/// identity at the swarm queue, minted here so that the credential an agent
/// presents names *it* rather than its hive;
/// 4. write the shared ACL document [`policy::render_shared_agent`] renders;
/// 5. write the cert-auth role that ties the three together, attaching that
/// policy and pinning the CA the store named as this leaf's issuer;
/// 6. name the agent's identity entity after the agent ([`crate::agent_entity`]),
/// which the policy reads.
/// 5. name the agent's identity entity after the agent ([`crate::agent_entity`]),
/// which the policy reads;
/// 6. write the cert-auth role that ties the three together, attaching that
/// policy and pinning the CA the store named as this leaf's issuer.
///
/// Policy before role, for the reason `read_policy::provision` gives.
/// Policy and entity before role: see [`publish_access`].
///
/// ⚠️ **Step 3 is idempotent and steps 1–2 are not.** Re-running issues a
/// fresh leaf, picked up on the agent's next boot, but leaves an existing
@ -135,10 +178,10 @@ pub(crate) fn generate_queue_secret() -> Result<String> {
/// replacing one by age is [`crate::agent_renewal`]'s.
///
/// # Errors
/// Anything that stops steps 1–5, with the step named, or the read-back login,
/// which is where a step 6 that did not take shows up. A failure here fails the
/// job node and nothing else — the agent is still created, without a store
/// identity.
/// Anything that stops one of those six steps, with the step named, or the
/// read-back login. A failure here fails the job node and nothing else — a new
/// agent is still created, without a store identity, and a live agent keeps the
/// role it had.
pub async fn mint_and_verify(agent: &str) -> Result<()> {
let (mount, pki_role) = agent_pki(|k| std::env::var(k).ok())?;
let name = policy::agent_object_name(agent)?;
@ -197,33 +240,8 @@ pub async fn mint_and_verify(agent: &str) -> Result<()> {
let queue_credential = wanted;
let inputs = role_inputs(agent, &credential)?;
store
.write_policy(policy::SHARED_AGENT_POLICY, &policy::render_shared_agent())
.await
.with_context(|| {
format!(
"writing the shared agent policy {}",
policy::SHARED_AGENT_POLICY
)
})?;
store
.write_cert_role(
DEFAULT_CERT_MOUNT,
&inputs.name,
inputs.ca,
&inputs.name,
&inputs
.attached
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
)
.await
.with_context(|| format!("writing the cert-auth role {}", inputs.name))?;
publish_access(&store, agent, &inputs).await?;
tracing::info!(agent, %path, role = %name, "agent store identity published");
// Before the read-back below, whose login would otherwise create the
// entity under a generated name.
crate::agent_entity::ensure_agent(&store, agent).await;
read_back_as_agent(&credential, &name, &path, &queue_path, &queue_credential).await?;
tracing::info!(
@ -352,8 +370,9 @@ async fn read_back_as_agent(
mod tests {
use super::{
ENV_AGENT_PKI_MOUNT, ENV_AGENT_PKI_ROLE, QUEUE_SECRET_BYTES, agent_pki,
generate_queue_secret, role_inputs,
generate_queue_secret, publish_access, role_inputs,
};
use crate::store::fake::FakeAccess;
use swarm_secret_client::{mtls, policy};
/// The alphabet claim the token format rests on: the secret is carried in
@ -481,4 +500,73 @@ mod tests {
assert_ne!(other.name, inputs.name);
assert_eq!(other.attached, inputs.attached);
}
fn credential() -> mtls::Credential {
mtls::Credential {
cert: "LEAF".to_owned(),
key: "KEY".to_owned(),
ca: "AGENT-CA".to_owned(),
}
}
/// The control: with a working entity step the role is narrowed, after the
/// entity is named, so the absence checks below can see a role write.
#[tokio::test]
async fn the_role_is_narrowed_after_the_entity_is_named() {
let store = FakeAccess::default();
let credential = credential();
let inputs = role_inputs("atlas", &credential).expect("legal");
publish_access(&store, "atlas", &inputs)
.await
.expect("a store that accepts everything");
assert_eq!(
store.roles_written(),
["write_cert_role hive-agent-atlas hive-agent"]
);
let calls = store.calls();
let at = |call: &str| calls.iter().position(|c| c == call).expect(call);
assert!(
at("create_entity_alias hive-agent-atlas")
< at("write_cert_role hive-agent-atlas hive-agent"),
"the entity is named before the role is written: {calls:?}"
);
}
#[tokio::test]
async fn a_renewal_whose_entity_step_fails_leaves_the_role_alone() {
let store = FakeAccess::without_accessor();
let credential = credential();
let inputs = role_inputs("atlas", &credential).expect("legal");
let err = publish_access(&store, "atlas", &inputs)
.await
.expect_err("the entity step failed");
assert!(
format!("{err:#}").contains("the role is not changed"),
"{err:#}"
);
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
// The control: the call log sees writes, and the shared policy was one.
assert!(
store
.calls()
.contains(&"write_policy hive-agent".to_owned())
);
}
#[tokio::test]
async fn a_renewal_whose_alias_is_on_another_entity_leaves_the_role_alone() {
let store = FakeAccess::with_alias_on("id-1", "somebody");
let credential = credential();
let inputs = role_inputs("atlas", &credential).expect("legal");
let err = publish_access(&store, "atlas", &inputs)
.await
.expect_err("a conflict is an error");
assert!(format!("{err:#}").contains("somebody"), "{err:#}");
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
assert!(
store
.calls()
.contains(&"entity_by_alias hive-agent-atlas".to_owned())
);
}
}

View file

@ -29,7 +29,9 @@ use std::{future::Future, time::Duration};
use anyhow::{Context, Result};
use backon::{BackoffBuilder as _, ConstantBuilder};
use swarm_secret_client::{Error, SecretStore, client::DEFAULT_CERT_MOUNT, policy};
use swarm_secret_client::{Error, client::DEFAULT_CERT_MOUNT, policy};
use crate::store::AccessStore;
/// File holding the authority hives are issued from, as
/// `swarm-controller.nix` names it.
@ -154,21 +156,38 @@ async fn provision(hives: &[String]) -> Result<(), Unreachable> {
}
Err(e) => return Err(Unreachable(e.to_string())),
};
if !write_shared_policy(&store).await {
return Ok(());
}
provision_with(&store, hives, read_hive_authority).await
}
/// The hive authority from the file `ENV_HIVE_CLIENT_CA` names, or `None` when
/// none is configured.
fn read_hive_authority() -> Result<Option<String>, Unreachable> {
let Some(path) = std::env::var_os(ENV_HIVE_CLIENT_CA) else {
tracing::info!("no hive certificate authority configured; hive roles are not managed here");
return Ok(());
return Ok(None);
};
let path = path.to_string_lossy().into_owned();
// Retryable rather than fatal: the unit that places this file and this
// daemon start together, so "not there yet" is a moment, not a verdict.
let ca = std::fs::read_to_string(&path)
.map_err(|e| Unreachable(format!("reading the hive authority {path}: {e}")))?;
std::fs::read_to_string(&path)
.map(Some)
.map_err(|e| Unreachable(format!("reading the hive authority {path}: {e}")))
}
/// [`provision`] against `store`, with the authority read by `read_authority`.
async fn provision_with(
store: &impl AccessStore,
hives: &[String],
read_authority: impl FnOnce() -> Result<Option<String>, Unreachable>,
) -> Result<(), Unreachable> {
if !write_shared_policy(store).await {
return Ok(());
}
let Some(ca) = read_authority()? else {
tracing::info!("no hive certificate authority configured; hive roles are not managed here");
return Ok(());
};
for hive in hives {
if let Err(e) = write_role(&store, hive, &ca).await {
if let Err(e) = write_role(store, hive, &ca).await {
tracing::warn!(hive, error = %format!("{e:#}"), "creating this hive's store role failed");
}
}
@ -180,7 +199,7 @@ async fn provision(hives: &[String]) -> Result<(), Unreachable> {
///
/// A store that refuses the accessor read (the controller's policy may not
/// carry it yet) or the write leaves the roles already in the store as they are.
async fn write_shared_policy(store: &SecretStore) -> bool {
async fn write_shared_policy(store: &impl AccessStore) -> bool {
let accessor = match store.auth_mount_accessor(DEFAULT_CERT_MOUNT).await {
Ok(accessor) => accessor,
Err(e) => {
@ -227,7 +246,7 @@ fn attached_policies() -> Vec<String> {
}
/// The role for one hive: its own certificate's subject and the shared policy.
async fn write_role(store: &SecretStore, hive: &str, ca: &str) -> Result<()> {
async fn write_role(store: &impl AccessStore, hive: &str, ca: &str) -> Result<()> {
let name = policy::hive_object_name(hive)?;
let attached = attached_policies();
store
@ -330,9 +349,11 @@ async fn rewrite_agent_policies() -> Result<(), Unreachable> {
mod tests {
use std::{cell::Cell, time::Duration};
use crate::store::fake::FakeAccess;
use super::{
RETRY_ATTEMPTS, RETRY_INTERVAL, RETRY_WINDOW, Unreachable, attached_policies, policy,
retry_until_ok,
provision_with, retry_until_ok,
};
#[test]
@ -385,4 +406,84 @@ mod tests {
// The control: the same lookup finds a policy that is attached.
assert!(attached.contains(&policy::SHARED_HIVE_POLICY.to_owned()));
}
fn hives() -> Vec<String> {
["pr1ma", "beta"].map(str::to_owned).to_vec()
}
#[tokio::test]
async fn every_hives_role_is_written_after_the_shared_policy_and_attaches_it_alone() {
let store = FakeAccess::default();
provision_with(&store, &hives(), || Ok(Some("HIVE-CA".to_owned())))
.await
.unwrap_or_else(|_| panic!("a store that accepts everything"));
assert_eq!(
store.roles_written(),
[
"write_cert_role hive-pr1ma hive-hives",
"write_cert_role hive-beta hive-hives"
]
);
let calls = store.calls();
let policy_at = calls
.iter()
.position(|c| c == "write_policy hive-hives")
.expect("the shared policy was written");
let first_role_at = calls
.iter()
.position(|c| c.starts_with("write_cert_role "))
.expect("a role was written");
assert!(policy_at < first_role_at, "{calls:?}");
}
#[tokio::test]
async fn an_unreadable_accessor_leaves_every_hive_role_as_it_was() {
let store = FakeAccess::without_accessor();
provision_with(&store, &hives(), || Ok(Some("HIVE-CA".to_owned())))
.await
.unwrap_or_else(|_| panic!("a policy that cannot be written is not a retry"));
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
// The control: the accessor read was attempted, and the same fake
// writes roles when it works (the test above).
assert_eq!(store.calls(), ["auth_mount_accessor"]);
}
#[tokio::test]
async fn a_refused_shared_policy_leaves_every_hive_role_as_it_was() {
let store = FakeAccess::refusing_policies();
provision_with(&store, &hives(), || Ok(Some("HIVE-CA".to_owned())))
.await
.unwrap_or_else(|_| panic!("a refused write is not a retry"));
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
assert!(
store
.calls()
.contains(&"write_policy hive-hives".to_owned())
);
}
#[tokio::test]
async fn without_an_authority_the_policy_is_written_and_no_role_is() {
let store = FakeAccess::default();
provision_with(&store, &hives(), || Ok(None))
.await
.unwrap_or_else(|_| panic!("no authority configured is not a retry"));
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
assert!(
store
.calls()
.contains(&"write_policy hive-hives".to_owned())
);
}
#[tokio::test]
async fn an_authority_that_cannot_be_read_is_retried_and_writes_no_role() {
let store = FakeAccess::default();
let outcome = provision_with(&store, &hives(), || {
Err(Unreachable("not placed yet".to_owned()))
})
.await;
assert!(outcome.is_err());
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
}
}

View file

@ -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(())
}
}
}