Watch
0
0
Fork
You've already forked hyperhive
0

add one shared templated agent policy and name each agent's bao entity after it

This commit is contained in:
atlas 2026-10-25 20:39:59 +01:00 • committed by mara
commit 4cdf807afc

View file

@ -3,7 +3,7 @@
use std::time::Duration;
use rustify_derive::Endpoint;
use serde::{Serialize, de::DeserializeOwned};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use vaultrs::{
api::pki::{requests::GenerateCertificateRequest, responses::GenerateCertificateResponse},
client::{Client, VaultClient, VaultClientSettings, VaultClientSettingsBuilder},
@ -355,7 +355,7 @@ impl SecretStore {
/// Create or replace the cert-auth role `name`, so that a certificate
/// issued by `certificate` and carrying `common_name` logs in with
/// `policy`.
/// `policies`.
///
/// `certificate` is the authority by value, not a path: the store keeps
/// its own copy inside the role and never reads a file of ours.
@ -369,12 +369,12 @@ impl SecretStore {
name: &str,
certificate: &str,
common_name: &str,
policy: &str,
policies: &[&str],
) -> Result<(), Error> {
let mut opts =
vaultrs::api::auth::cert::requests::CreateCaCertificateRoleRequest::builder();
opts.allowed_common_names(vec![common_name.to_owned()])
.token_policies(vec![policy.to_owned()]);
.token_policies(policies.iter().map(|p| (*p).to_owned()).collect::<Vec<_>>());
vaultrs::auth::cert::ca_cert_role::set(
&self.inner,
mount,
@ -428,6 +428,195 @@ impl SecretStore {
Err(e) => Err(e.into()),
}
}
/// The accessor of the auth method mounted at `mount`: the id an entity
/// alias names that mount by.
///
/// # Errors
/// [`Error::Vault`] when the token's policy does not grant `read` and
/// `sudo` on `sys/auth/<mount>` (a plain `read` is refused), or no such
/// mount exists.
pub async fn auth_mount_accessor(&self, mount: &str) -> Result<String, Error> {
let info = vaultrs::api::exec_with_result(
&self.inner,
ReadAuthMount {
mount: mount.to_owned(),
},
)
.await?;
Ok(info.accessor)
}
/// The identity entity that the alias `alias_name` on the auth mount
/// `mount_accessor` belongs to, or `None` when neither a login nor a
/// caller has created that alias.
///
/// # Errors
/// [`Error::Vault`] for anything but an empty answer: a denial, or an
/// unreachable store.
pub async fn entity_by_alias(
&self,
alias_name: &str,
mount_accessor: &str,
) -> Result<Option<EntityRef>, Error> {
let request = LookupEntity {
alias_name: alias_name.to_owned(),
alias_mount_accessor: mount_accessor.to_owned(),
};
match vaultrs::api::exec_with_result(&self.inner, request).await {
Ok(entity) => Ok(Some(entity)),
// A lookup that finds nothing is answered with an empty 204, which
// does not parse as the envelope. Any other parse failure stays one.
Err(vaultrs::error::ClientError::RestClientError {
source: rustify::errors::ClientError::ResponseParseError { content, .. },
}) if content.as_deref().is_none_or(str::is_empty) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// The id of the identity entity named `name`, or `None` when there is
/// none. Only a 404 is absence, for the reason
/// [`read_optional`][Self::read_optional] gives.
///
/// # Errors
/// [`Error::Vault`] for anything that is not a 404.
pub async fn entity_id_by_name(&self, name: &str) -> Result<Option<String>, Error> {
let request = ReadEntityByName {
name: name.to_owned(),
};
match vaultrs::api::exec_with_result(&self.inner, request).await {
Ok(entity) => Ok(Some(entity.id)),
Err(e) if is_absent(&e) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Create the identity entity `name` with no policies, metadata or
/// aliases; one that exists is left as it is.
///
/// # Errors
/// [`Error::Vault`] when the token's policy does not cover
/// `identity/entity/name/<name>`.
pub async fn create_entity(&self, name: &str) -> Result<(), Error> {
let request = CreateEntityByName {
name: name.to_owned(),
};
vaultrs::api::exec_with_empty(&self.inner, request).await?;
Ok(())
}
/// Give the identity entity `id` the name `name`.
///
/// # Errors
/// [`Error::Vault`] when another entity already has that name, or the
/// token's policy does not cover `identity/entity/id/<id>`.
pub async fn rename_entity(&self, id: &str, name: &str) -> Result<(), Error> {
let request = RenameEntity {
id: id.to_owned(),
name: name.to_owned(),
};
vaultrs::api::exec_with_empty(&self.inner, request).await?;
Ok(())
}
/// Attach the alias `alias_name` on the auth mount `mount_accessor` to
/// the entity `entity_id`, so that a login under that name lands on it.
///
/// # Errors
/// [`Error::Vault`] when the store refuses, or the token's policy does
/// not cover `identity/entity-alias`.
pub async fn create_entity_alias(
&self,
alias_name: &str,
mount_accessor: &str,
entity_id: &str,
) -> Result<(), Error> {
let request = CreateEntityAlias {
name: alias_name.to_owned(),
mount_accessor: mount_accessor.to_owned(),
canonical_id: entity_id.to_owned(),
};
vaultrs::api::exec_with_empty(&self.inner, request).await?;
Ok(())
}
}
/// An identity entity as a lookup names it.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct EntityRef {
/// The store's id for the entity.
pub id: String,
/// The entity's name: generated by the store when a login created the
/// entity, the caller's when the caller did.
pub name: String,
}
/// What this crate reads of `sys/auth/<mount>`.
#[derive(Deserialize)]
struct AuthMountInfo {
accessor: String,
}
/// Spelled out here because [`vaultrs`] can list the auth mounts but not read
/// one, and the list needs the wider `sys/auth` grant.
#[derive(Endpoint)]
#[endpoint(
path = "sys/auth/{self.mount}",
method = "GET",
response = "AuthMountInfo"
)]
struct ReadAuthMount {
#[endpoint(skip)]
mount: String,
}
#[derive(Endpoint)]
#[endpoint(
path = "identity/lookup/entity",
method = "POST",
response = "EntityRef"
)]
struct LookupEntity {
alias_name: String,
alias_mount_accessor: String,
}
/// Spelled out here because [`vaultrs`]'s response for this read requires
/// fields this crate has no use for.
#[derive(Endpoint)]
#[endpoint(
path = "identity/entity/name/{self.name}",
method = "GET",
response = "EntityRef"
)]
struct ReadEntityByName {
#[endpoint(skip)]
name: String,
}
/// The name in the path is all that is sent, which is what the controller's
/// parameter-restricted grant allows.
#[derive(Endpoint)]
#[endpoint(path = "identity/entity/name/{self.name}", method = "POST")]
struct CreateEntityByName {
#[endpoint(skip)]
name: String,
}
#[derive(Endpoint)]
#[endpoint(path = "identity/entity/id/{self.id}", method = "POST")]
struct RenameEntity {
#[endpoint(skip)]
id: String,
name: String,
}
#[derive(Endpoint)]
#[endpoint(path = "identity/entity-alias", method = "POST")]
struct CreateEntityAlias {
name: String,
mount_accessor: String,
canonical_id: String,
}
/// Whether a failed store call means the path holds nothing — the one status
@ -786,4 +975,74 @@ mod tests {
assert_eq!(soft.path(), "secret/data/swarm/agents/atlas/queue");
assert_ne!(request.path(), soft.path());
}
/// The bodies the controller's parameter-restricted grant
/// (`swarm-bao.nix`'s `controllerPolicyText`) allows: any other key in a
/// request is refused by the store, so a field added to one of these
/// structs fails here before it fails there.
#[test]
fn the_identity_requests_send_only_the_keys_the_grant_allows() {
use rustify::endpoint::Endpoint as _;
let keys = |body: Option<Vec<u8>>| -> Vec<String> {
let value: serde_json::Value =
serde_json::from_slice(&body.unwrap_or_else(|| b"{}".to_vec())).expect("json");
let mut keys: Vec<String> = value
.as_object()
.expect("an object")
.keys()
.cloned()
.collect();
keys.sort();
keys
};
let create = CreateEntityByName {
name: "atlas".to_owned(),
};
assert_eq!(create.path(), "identity/entity/name/atlas");
assert!(keys(create.body().expect("body")).is_empty());
let rename = RenameEntity {
id: "e1".to_owned(),
name: "atlas".to_owned(),
};
assert_eq!(rename.path(), "identity/entity/id/e1");
assert_eq!(keys(rename.body().expect("body")), ["name"]);
let alias = CreateEntityAlias {
name: "hive-agent-atlas".to_owned(),
mount_accessor: "auth_cert_x".to_owned(),
canonical_id: "e1".to_owned(),
};
assert_eq!(alias.path(), "identity/entity-alias");
assert_eq!(
keys(alias.body().expect("body")),
["canonical_id", "mount_accessor", "name"]
);
let lookup = LookupEntity {
alias_name: "hive-agent-atlas".to_owned(),
alias_mount_accessor: "auth_cert_x".to_owned(),
};
assert_eq!(lookup.path(), "identity/lookup/entity");
assert_eq!(
keys(lookup.body().expect("body")),
["alias_mount_accessor", "alias_name"]
);
assert_eq!(
ReadAuthMount {
mount: "cert".to_owned()
}
.path(),
"sys/auth/cert"
);
assert_eq!(
ReadEntityByName {
name: "atlas".to_owned()
}
.path(),
"identity/entity/name/atlas"
);
}
}