docs: drop harness systemd settings; swarm-nats-auth: refuse the hive's shared agent client
Docs (review on #4948): the harness's restart settings are not useful to a reader, so the `Restart`/`RestartSec` bullet in docs/turn-loop and the "30 seconds later, no start limit" wording in docs/swarm are removed. What stays is the behaviour: the harness exits and comes back until the queue accepts it. Grant: with the harness's shared-client fallback gone, nothing presents the hive's shared agent client (`hive-<hive>-agent`) to the queue; swarm-logs uses that OIDC client over HTTP only. swarm-nats-auth now refuses it outright instead of granting the configured `$SWARM.term.{hive}.>` / `$SWARM.agent-state.{hive}.>` publish and its `_INBOX.client.<id>.>` subscribe, so an agent holding it can no longer publish as a sibling. The agent arm still runs before the hive arm, so that id never falls through to its hive's grant. Removed with it: the `--agent-publish-subject` flag and its two uses in swarm-nats.nix, `Policy`'s agent subject list, and swarm-queue-client's `client_inbox_prefix`. The module-eval now asserts the flag is absent; the policy tests assert the refusal and that the hive itself is still admitted.
This commit is contained in:
parent
235bc481b3
commit
91fdf96a67
10 changed files with 85 additions and 333 deletions
|
|
@ -227,7 +227,6 @@ mod tests {
|
|||
"hive-status".to_owned(),
|
||||
vec!["swarm-controller".to_owned()],
|
||||
vec![],
|
||||
vec!["$SWARM.term.{hive}.>".to_owned()],
|
||||
)
|
||||
.expect("valid")
|
||||
.with_agent_token_subjects(vec![
|
||||
|
|
@ -355,7 +354,6 @@ mod tests {
|
|||
"hive-status".to_owned(),
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
)
|
||||
.expect("valid");
|
||||
let Presented::Agent(token) = classify("swarm-agent.atlas.Ab9_-zSECRET") else {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ struct Args {
|
|||
/// config change plus a reload.
|
||||
///
|
||||
/// So this identity says which hive an agent belongs to and never which
|
||||
/// agent: two agents on one hive are indistinguishable under it.
|
||||
/// agent: two agents on one hive are indistinguishable under it. The
|
||||
/// responder refuses it; the suffix is what tells it apart from a hive.
|
||||
///
|
||||
/// A suffix on the hive's id rather than a prefix of its own, because
|
||||
/// `agent-<name>` reads as *the agent called `<name>`* — the one thing
|
||||
|
|
@ -118,18 +119,6 @@ struct Args {
|
|||
#[arg(long = "hive-publish-subject")]
|
||||
hive_publish_subjects: Vec<String>,
|
||||
|
||||
/// Subjects an agent may publish to, with `{hive}` standing for the hive
|
||||
/// its identity names — `hive-alpha-agent` expands it to `alpha`.
|
||||
/// Repeatable, empty by default.
|
||||
///
|
||||
/// Empty means agents get **no grant at all** rather than a grant that can
|
||||
/// do nothing — the identity exists, and what it may say is a deployment's
|
||||
/// decision, not this responder's. The first consumer is the agent
|
||||
/// terminal-event stream; until one is configured, an agent that connects
|
||||
/// is refused, which is loud rather than silently over-broad.
|
||||
#[arg(long = "agent-publish-subject")]
|
||||
agent_publish_subjects: Vec<String>,
|
||||
|
||||
/// Subjects an agent that presented its own credential may publish to,
|
||||
/// with `{agent}` standing for its name. Repeatable, empty by default,
|
||||
/// which denies every agent token.
|
||||
|
|
@ -190,7 +179,6 @@ async fn main() -> anyhow::Result<()> {
|
|||
swarm_queue_client::status::BUCKET.to_owned(),
|
||||
args.reader_clients.clone(),
|
||||
args.hive_publish_subjects.clone(),
|
||||
args.agent_publish_subjects.clone(),
|
||||
)?
|
||||
.with_agent_token_subjects(args.agent_token_publish_subjects.clone())?;
|
||||
let store = agent_token::Store::from_env(args.store_cert_role.clone());
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ pub struct Permissions {
|
|||
/// account.
|
||||
///
|
||||
/// `Some` only for agents, the least trusted clients: their own reply
|
||||
/// inbox, plus their own input subject for an agent's own credential.
|
||||
/// inbox and their own input subject.
|
||||
/// Hives and readers get `None`. Never `Some(vec![])`: NATS reads an empty
|
||||
/// allow-list as no restriction at all.
|
||||
pub subscribe: Option<Vec<String>>,
|
||||
|
|
@ -45,12 +45,10 @@ pub struct Policy {
|
|||
bucket: String,
|
||||
readers: Vec<String>,
|
||||
extra_hive_subjects: Vec<String>,
|
||||
extra_agent_subjects: Vec<String>,
|
||||
agent_token_subjects: Vec<String>,
|
||||
}
|
||||
|
||||
/// Placeholder replaced with the hive's own name in `extra_hive_subjects` and
|
||||
/// `extra_agent_subjects`.
|
||||
/// Placeholder replaced with the hive's own name in `extra_hive_subjects`.
|
||||
const HIVE_PLACEHOLDER: &str = "{hive}";
|
||||
|
||||
/// Placeholder replaced with the agent's own name in `agent_token_subjects`.
|
||||
|
|
@ -61,9 +59,8 @@ impl Policy {
|
|||
/// `agent_suffix` what a hive's agent containers carry **on top of** it —
|
||||
/// `hive-<name>` and `hive-<name>-agent`. `bucket` is the KV bucket hives
|
||||
/// report status in, `readers` the client ids allowed to read every hive's
|
||||
/// key, and `extra_hive_subjects` / `extra_agent_subjects` additional
|
||||
/// subjects each may publish to (with `{hive}` standing for the hive's own
|
||||
/// name).
|
||||
/// key, and `extra_hive_subjects` additional subjects a hive may publish to
|
||||
/// (with `{hive}` standing for the hive's own name).
|
||||
///
|
||||
/// The agent id is deliberately the hive's id extended, not a second
|
||||
/// top-level prefix: `agent-<name>` reads as *the agent called `<name>`*,
|
||||
|
|
@ -88,7 +85,6 @@ impl Policy {
|
|||
bucket: String,
|
||||
readers: Vec<String>,
|
||||
extra_hive_subjects: Vec<String>,
|
||||
extra_agent_subjects: Vec<String>,
|
||||
) -> anyhow::Result<Self> {
|
||||
if let Some(bad) = extra_hive_subjects
|
||||
.iter()
|
||||
|
|
@ -100,32 +96,16 @@ impl Policy {
|
|||
a per-hive namespace"
|
||||
);
|
||||
}
|
||||
// Same rule, same reason: an agent's identity is its *hive's*, so a
|
||||
// template with no placeholder is one subject shared by every agent in
|
||||
// the swarm rather than a per-hive one.
|
||||
if let Some(bad) = extra_agent_subjects
|
||||
.iter()
|
||||
.find(|s| !s.contains(HIVE_PLACEHOLDER))
|
||||
{
|
||||
anyhow::bail!(
|
||||
"--agent-publish-subject {bad:?} contains no {HIVE_PLACEHOLDER}: every agent \
|
||||
in the swarm would be granted that exact subject, so it is a shared channel \
|
||||
rather than a per-hive namespace"
|
||||
);
|
||||
}
|
||||
// Checked here rather than trusted from nix, because these are two
|
||||
// flags and nothing downstream compares them. An agent id is a hive id
|
||||
// plus this suffix, so an empty one makes `strip_suffix` succeed on
|
||||
// every hive id and the two principals become the same string. What
|
||||
// that costs is not a parse error but a **grant**: whichever arm runs
|
||||
// first answers for both, and a client that authenticates fine is
|
||||
// handed more than it should have — the one failure this module must
|
||||
// not have.
|
||||
// every hive id and the two principals become the same string. The
|
||||
// agent arm runs first and refuses, so every hive would be refused as
|
||||
// its own agents' client.
|
||||
if agent_suffix.is_empty() {
|
||||
anyhow::bail!(
|
||||
"--agent-client-suffix is empty: an agent id is a hive id plus this suffix, \
|
||||
so every hive would also parse as its own agents and one of the two would \
|
||||
silently receive the other's grant"
|
||||
so every hive would also parse as its own agents' client and be refused"
|
||||
);
|
||||
}
|
||||
Ok(Self {
|
||||
|
|
@ -134,7 +114,6 @@ impl Policy {
|
|||
bucket,
|
||||
readers,
|
||||
extra_hive_subjects,
|
||||
extra_agent_subjects,
|
||||
agent_token_subjects: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
|
@ -193,24 +172,12 @@ impl Policy {
|
|||
// should. Most specific wins, so the more specific test is the one
|
||||
// that has to be asked first.
|
||||
//
|
||||
// An agent's identity names its hive, never the agent — see
|
||||
// `Self::agent_hive`. The grant is whatever the deployment configured
|
||||
// for agents, expanded for that hive, and **nothing when it configured
|
||||
// none**: an empty publish list would be a denial wearing a grant's
|
||||
// shape, so this returns `None` and the client is refused outright.
|
||||
//
|
||||
// Subscribe is its own reply inbox alone. The identity names no agent,
|
||||
// so it cannot be granted one agent's input subject without all of
|
||||
// them.
|
||||
if let Some(hive) = self.agent_hive(client_id) {
|
||||
let publish = self.agent_subjects(hive);
|
||||
return (!publish.is_empty()).then(|| Permissions {
|
||||
publish,
|
||||
subscribe: Some(vec![format!(
|
||||
"{}.>",
|
||||
swarm_queue_client::client_inbox_prefix(client_id)
|
||||
)]),
|
||||
});
|
||||
// A hive's shared agent client is refused outright. Agents connect with
|
||||
// their own credential (`Self::agent_token_permissions`); this identity
|
||||
// names the hive and never the agent (see `Self::agent_hive`), so any
|
||||
// grant to it would let one agent publish as its siblings.
|
||||
if self.agent_hive(client_id).is_some() {
|
||||
return None;
|
||||
}
|
||||
if let Some(hive) = self.hive_name(client_id) {
|
||||
return Some(Permissions {
|
||||
|
|
@ -459,19 +426,6 @@ impl Policy {
|
|||
subjects
|
||||
}
|
||||
|
||||
/// Subjects an agent of `hive` may publish to.
|
||||
///
|
||||
/// No `JetStream` minimum, unlike [`Self::hive_subjects`]: a hive needs it
|
||||
/// because it writes the status bucket, and an agent publishes to plain
|
||||
/// subjects. Granting it anyway would be reasoning by analogy about a
|
||||
/// permission set the module documents as minimal by removal.
|
||||
fn agent_subjects(&self, hive: &str) -> Vec<String> {
|
||||
self.extra_agent_subjects
|
||||
.iter()
|
||||
.map(|s| s.replace(HIVE_PLACEHOLDER, hive))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn reader_subjects(&self) -> Vec<String> {
|
||||
let stream = self.stream();
|
||||
let mut subjects = Self::jetstream_minimum().to_vec();
|
||||
|
|
@ -583,26 +537,10 @@ mod tests {
|
|||
"hive-status".to_owned(),
|
||||
vec!["swarm-controller".to_owned()],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
.expect("the default policy is valid")
|
||||
}
|
||||
|
||||
/// The same policy with agents given something to publish, since the
|
||||
/// default deployment configures no agent subject and an agent is then
|
||||
/// refused outright.
|
||||
fn policy_with_agent_subject() -> Policy {
|
||||
Policy::new(
|
||||
"hive-".to_owned(),
|
||||
"-agent".to_owned(),
|
||||
"hive-status".to_owned(),
|
||||
vec!["swarm-controller".to_owned()],
|
||||
Vec::new(),
|
||||
vec!["$SWARM.term.{hive}.>".to_owned()],
|
||||
)
|
||||
.expect("a per-hive agent template is valid")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reader_can_open_and_list_the_agent_status_bucket() {
|
||||
let subjects = policy().reader_subjects();
|
||||
|
|
@ -701,7 +639,6 @@ mod tests {
|
|||
"hive-status".to_owned(),
|
||||
Vec::new(),
|
||||
vec!["$SWARM.events.all".to_owned()],
|
||||
Vec::new(),
|
||||
)
|
||||
.expect_err("a subject shared by every hive must not be accepted");
|
||||
let msg = format!("{err}");
|
||||
|
|
@ -721,7 +658,6 @@ mod tests {
|
|||
"hive-status".to_owned(),
|
||||
Vec::new(),
|
||||
vec!["$SWARM.events.{hive}.>".to_owned()],
|
||||
Vec::new(),
|
||||
)
|
||||
.expect("a per-hive template is the shape this option is for");
|
||||
}
|
||||
|
|
@ -818,29 +754,26 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn neither_a_hive_nor_its_agents_may_publish_input_to_an_agent() {
|
||||
fn a_hive_may_not_publish_input_to_an_agent() {
|
||||
// The harness turns this message into a turn from the operator named in
|
||||
// it. A hive or agent able to publish here could speak as the operator
|
||||
// to any agent in the swarm.
|
||||
let policy = policy_with_agent_subject();
|
||||
for client in ["hive-alpha", "hive-alpha-agent"] {
|
||||
let p = policy.permissions(client).expect("admitted");
|
||||
for forbidden in [
|
||||
swarm_queue_client::agent_input_subject("alpha"),
|
||||
swarm_queue_client::AGENT_INPUT_SUBJECT_WILDCARD.to_owned(),
|
||||
] {
|
||||
assert!(
|
||||
!p.publish.contains(&forbidden),
|
||||
"{client} must not publish {forbidden}: {:?}",
|
||||
p.publish
|
||||
);
|
||||
}
|
||||
// it. A hive able to publish here could speak as the operator to any
|
||||
// agent in the swarm.
|
||||
let p = policy().permissions("hive-alpha").expect("admitted");
|
||||
for forbidden in [
|
||||
swarm_queue_client::agent_input_subject("alpha"),
|
||||
swarm_queue_client::AGENT_INPUT_SUBJECT_WILDCARD.to_owned(),
|
||||
] {
|
||||
assert!(
|
||||
!p.publish.iter().any(|s| s.starts_with("$SWARM.input")),
|
||||
"{client}: {:?}",
|
||||
!p.publish.contains(&forbidden),
|
||||
"a hive must not publish {forbidden}: {:?}",
|
||||
p.publish
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!p.publish.iter().any(|s| s.starts_with("$SWARM.input")),
|
||||
"{:?}",
|
||||
p.publish
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -981,9 +914,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn an_agent_token_may_subscribe_to_its_own_inbox_and_input_only() {
|
||||
use swarm_queue_client::{agent_inbox_prefix, agent_input_subject, client_inbox_prefix};
|
||||
use swarm_queue_client::{agent_inbox_prefix, agent_input_subject};
|
||||
|
||||
let p = policy_with_agent_subject()
|
||||
let p = policy()
|
||||
.with_agent_token_subjects(vec!["$SWARM.term.{agent}".to_owned()])
|
||||
.expect("a per-agent template is valid");
|
||||
let atlas = p.agent_token_permissions("atlas").expect("configured");
|
||||
|
|
@ -997,7 +930,6 @@ mod tests {
|
|||
"$SWARM.input.*".to_owned(),
|
||||
agent_input_subject("argus"),
|
||||
reply(&agent_inbox_prefix("argus")),
|
||||
reply(&client_inbox_prefix("hive-alpha-agent")),
|
||||
"_INBOX.nuid123.token456".to_owned(),
|
||||
] {
|
||||
assert!(
|
||||
|
|
@ -1007,32 +939,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hives_shared_agent_client_may_subscribe_to_its_own_inbox_only() {
|
||||
use swarm_queue_client::{agent_inbox_prefix, agent_input_subject, client_inbox_prefix};
|
||||
|
||||
let p = policy_with_agent_subject();
|
||||
let alpha = p.permissions("hive-alpha-agent").expect("alpha's agents");
|
||||
assert!(may_subscribe(
|
||||
&alpha,
|
||||
&reply(&client_inbox_prefix("hive-alpha-agent"))
|
||||
));
|
||||
for refused in [
|
||||
">".to_owned(),
|
||||
"$SWARM.term.>".to_owned(),
|
||||
"$SWARM.term.alpha.atlas".to_owned(),
|
||||
agent_input_subject("atlas"),
|
||||
reply(&client_inbox_prefix("hive-beta-agent")),
|
||||
reply(&agent_inbox_prefix("atlas")),
|
||||
"_INBOX.nuid123.token456".to_owned(),
|
||||
] {
|
||||
assert!(
|
||||
!may_subscribe(&alpha, &refused),
|
||||
"alpha's agents may subscribe to {refused}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hives_and_readers_keep_an_unrestricted_subscribe() {
|
||||
let p = policy();
|
||||
|
|
@ -1252,7 +1158,6 @@ mod tests {
|
|||
"hive-status".to_owned(),
|
||||
Vec::new(),
|
||||
vec!["$SWARM.events.{hive}.>".to_owned()],
|
||||
Vec::new(),
|
||||
)
|
||||
.expect("a per-hive template is valid");
|
||||
let g = p.permissions("hive-alpha").expect("a hive is admitted");
|
||||
|
|
@ -1261,106 +1166,40 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_with_no_configured_subject_is_refused() {
|
||||
// The shipped default: the identity exists, what it may say does not.
|
||||
// A grant with an empty publish list would let it connect and then
|
||||
// fail on every publish, which reads as a broken queue rather than as
|
||||
// a deployment that configured nothing.
|
||||
assert!(policy().permissions("hive-alpha-agent").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_publishes_inside_its_own_hives_namespace() {
|
||||
// The presence control for the test above - without it, "refused"
|
||||
// would also be the answer if the agent arm never matched at all.
|
||||
let g = policy_with_agent_subject()
|
||||
.permissions("hive-alpha-agent")
|
||||
.expect("an agent with a configured subject is admitted");
|
||||
assert_eq!(g.publish, vec!["$SWARM.term.alpha.>".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_does_not_get_its_hives_grant() {
|
||||
// An agent holds a credential that sits in a container; a hive's grant
|
||||
// includes writing that hive's status key, which an agent must not be
|
||||
// able to forge.
|
||||
fn the_hives_shared_agent_client_is_refused() {
|
||||
// Agents connect with their own credential. The shared client names
|
||||
// the hive and never the agent, so any grant to it would let one agent
|
||||
// publish as its siblings.
|
||||
//
|
||||
// 🩸 This became a REGRESSION test when the id moved from `agent-<hive>`
|
||||
// to `hive-<hive>-agent`: the agent id now satisfies the HIVE parse too
|
||||
// (`hive-alpha-agent` strips to a hive named `alpha-agent`), so a
|
||||
// hive-first arm order hands every agent its hive's grant, and nothing
|
||||
// reports it — the client authenticates and is merely able to do more
|
||||
// than it should. Together with the `assert_eq` in
|
||||
// `an_agent_publishes_inside_its_own_hives_namespace` (which pins the
|
||||
// subject to `alpha`, not `alpha-agent`) this is what holds the
|
||||
// ordering in place.
|
||||
let g = policy_with_agent_subject()
|
||||
.permissions("hive-alpha-agent")
|
||||
.expect("an agent is admitted");
|
||||
assert!(!g.publish.iter().any(|s| s.starts_with("$KV.")));
|
||||
assert!(!g.publish.iter().any(|s| s.starts_with("$JS.API.")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_subject_without_the_placeholder_is_refused() {
|
||||
let err = Policy::new(
|
||||
"hive-".to_owned(),
|
||||
"-agent".to_owned(),
|
||||
"hive-status".to_owned(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
vec!["$SWARM.term.all".to_owned()],
|
||||
)
|
||||
.expect_err("a subject shared by every agent in the swarm is not per-hive");
|
||||
assert!(
|
||||
format!("{err}").contains("$SWARM.term.all"),
|
||||
"the error must name the offending template"
|
||||
);
|
||||
// 🩸 The agent id satisfies the HIVE parse too (`hive-alpha-agent`
|
||||
// strips to a hive named `alpha-agent`), so a hive-first arm order
|
||||
// would hand this client its hive's grant, status key included, and
|
||||
// nothing would report it. `is_none` here is what holds the order.
|
||||
assert!(policy().permissions("hive-alpha-agent").is_none());
|
||||
// Control: the hive itself is admitted, so the refusal above is the
|
||||
// agent arm answering rather than the prefix failing to match.
|
||||
assert!(policy().permissions("hive-alpha").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_agent_suffix_is_refused() {
|
||||
// 🩸 An agent id is a hive id plus the suffix, so an empty suffix makes
|
||||
// `strip_suffix` succeed on every hive id: the two principals become
|
||||
// one string, and whichever arm runs first answers for both. That is a
|
||||
// GRANT, not a parse error, which is why it has to fail at
|
||||
// construction.
|
||||
// one string, and the agent arm, which runs first and refuses, would
|
||||
// refuse every hive. That fails at construction rather than at connect.
|
||||
Policy::new(
|
||||
"hive-".to_owned(),
|
||||
String::new(),
|
||||
"hive-status".to_owned(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
.expect_err("an empty agent suffix must not construct");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_agent_suffix_alone_names_no_hive() {
|
||||
// `hive--agent` is the prefix and the suffix with nothing between them.
|
||||
// The agent arm must refuse it rather than expand `{hive}` to the empty
|
||||
// string and hand out `$SWARM.term..>`.
|
||||
//
|
||||
// 🩸 Written first as `is_none()`, by analogy with
|
||||
// `the_prefix_alone_names_no_hive`, and it FAILED — correctly. The
|
||||
// analogy does not hold: under the hive parse the same id is a hive
|
||||
// named `-agent`, which is a non-empty name this responder cannot rule
|
||||
// out, because it has no roster. `swarm-authelia.nix` refuses such a
|
||||
// name at eval, where the roster is known. So the assertion here is
|
||||
// narrowed to the only part this module owns.
|
||||
let g = policy_with_agent_subject().permissions("hive--agent");
|
||||
assert!(
|
||||
!g.iter()
|
||||
.flat_map(|p| &p.publish)
|
||||
.any(|s| s.contains("$SWARM.term..")),
|
||||
"an empty hive name must never be expanded into a subject: {g:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_token_grant_is_the_agents_own_subjects_and_nothing_else() {
|
||||
let p = policy_with_agent_subject()
|
||||
let p = policy()
|
||||
.with_agent_token_subjects(vec![
|
||||
"$SWARM.term.{agent}".to_owned(),
|
||||
"$SWARM.agent-state.{agent}".to_owned(),
|
||||
|
|
@ -1376,14 +1215,12 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// A verified agent may write its own icon key and no other agent's. The
|
||||
/// hive's shared agent client, which every agent on the hive presents, may
|
||||
/// write none of the bucket.
|
||||
/// A verified agent may write its own icon key and no other agent's.
|
||||
#[test]
|
||||
fn an_agent_may_write_only_its_own_icon_key() {
|
||||
use swarm_queue_client::agent_icon::{BUCKET, subject};
|
||||
use swarm_queue_client::agent_icon::subject;
|
||||
|
||||
let p = policy_with_agent_subject()
|
||||
let p = policy()
|
||||
.with_agent_token_subjects(vec!["$KV.agent-icons.{agent}".to_owned()])
|
||||
.expect("a per-agent template is valid");
|
||||
let atlas = p.agent_token_permissions("atlas").expect("configured");
|
||||
|
|
@ -1393,15 +1230,6 @@ mod tests {
|
|||
// lacking it is atlas's grant being scoped.
|
||||
let argus = p.agent_token_permissions("argus").expect("configured");
|
||||
assert_eq!(argus.publish, vec![subject("argus")]);
|
||||
|
||||
let shared = p.permissions("hive-alpha-agent").expect("alpha's agents");
|
||||
assert!(
|
||||
!shared
|
||||
.publish
|
||||
.iter()
|
||||
.any(|s| s.starts_with(&format!("$KV.{BUCKET}."))),
|
||||
"the hive's shared agent client got an icon key: {shared:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The subagent template as `swarm-nats.nix` spells it expands to the
|
||||
|
|
@ -1411,7 +1239,7 @@ mod tests {
|
|||
fn an_agents_subagent_grant_is_publish_on_its_own_family_only() {
|
||||
use swarm_queue_client::subagent_term::{stream_name, stream_subjects, subject};
|
||||
|
||||
let p = policy_with_agent_subject()
|
||||
let p = policy()
|
||||
.with_agent_token_subjects(vec!["$SWARM.term.{agent}.sub.>".to_owned()])
|
||||
.expect("a per-agent template is valid");
|
||||
let g = p.agent_token_permissions("atlas").expect("configured");
|
||||
|
|
@ -1435,15 +1263,12 @@ mod tests {
|
|||
|
||||
/// The schedule template as `swarm-nats.nix` spells it reads one key: the
|
||||
/// agent's own. It covers no other agent's key, no other stream or bucket,
|
||||
/// and no way to open, watch or write the bucket. The hive's shared agent
|
||||
/// client gets none of it.
|
||||
/// and no way to open, watch or write the bucket.
|
||||
#[test]
|
||||
fn an_agent_may_read_only_its_own_schedule_key() {
|
||||
use swarm_queue_client::agent_schedules::{
|
||||
BUCKET, STREAM, direct_get_subject, key_subject,
|
||||
};
|
||||
use swarm_queue_client::agent_schedules::{STREAM, direct_get_subject, key_subject};
|
||||
|
||||
let p = policy_with_agent_subject()
|
||||
let p = policy()
|
||||
.with_agent_token_subjects(vec![
|
||||
"$JS.API.DIRECT.GET.KV_agent-schedules.$KV.agent-schedules.{agent}".to_owned(),
|
||||
])
|
||||
|
|
@ -1483,12 +1308,6 @@ mod tests {
|
|||
// vacuous.
|
||||
assert!(covers(&atlas.publish[0], &direct_get_subject("atlas")));
|
||||
assert!(covers("$KV.*.>", &key_subject("argus")));
|
||||
|
||||
let shared = p.permissions("hive-alpha-agent").expect("alpha's agents");
|
||||
assert!(
|
||||
!shared.publish.iter().any(|s| s.contains(BUCKET)),
|
||||
"the hive's shared agent client got a schedule subject: {shared:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue