#[test]
fn a_reveal_argument_makes_a_secret_read() {
let t = Tool {
name: "database_get".into(),
title: None,
description: String::new(),
input_schema: serde_json::json!({"type": "object"}),
annotations: Some(serde_json::json!({"readOnlyHint": true, "isbSecretReadArg": "reveal"})),
handler: std::sync::Arc::new(|_, _| Ok(Value::Null)),
};
let plain = class_for(&t, &serde_json::json!({"name": "pg"}));
assert!(plain.read_only && !plain.secret_read);
let reveal = class_for(&t, &serde_json::json!({"name": "pg", "reveal": true}));
assert!(!reveal.read_only && reveal.secret_read);
assert!(scope_allows(&["read".to_string()], "database_get", plain));
assert!(!scope_allows(&["read".to_string()], "database_get", reveal));
}
use super::*;
use crate::auth::Role;
use crate::server::ToolPolicy;
use crate::server::http::Peer;
use crate::server::mcp::{Authenticated, Endpoint, Hooks};
use std::collections::HashMap;
const SECRET: &str = "hunter2-very-secret-value";
struct T {
ep: Endpoint,
log: Arc<AuditLog>,
_dir: tempfile::TempDir,
}
fn endpoint() -> T {
use super::super::tests::{token, user};
let dir = tempfile::tempdir().unwrap();
let log = Arc::new(
AuditLog::open(
&dir.path().join("audit.db"),
crate::audit::DEFAULT_RETENTION,
)
.unwrap(),
);
let mut r = Registry::new();
let ro = json!({"readOnlyHint": true});
let write = json!({"destructiveHint": false});
for (name, ann) in [
("secret_set", &write),
("stack_deploy", &write),
("sandbox_exec", &write),
("stack_status", &ro),
("secret_get", &ro),
] {
r.register(
Tool::new(name, "", json!({}), |a: Value, _c: &Caller| {
Ok(json!({"echo": a.get("name"), "value": "c2VjcmV0"}))
})
.annotations(ann.clone()),
)
.unwrap();
}
register(&mut r, log.clone()).unwrap();
let callers: HashMap<&str, Caller> = HashMap::from([
("agent", token(&[("acme", Role::Member)], &[])),
("ro", token(&[("acme", Role::Member)], &["read"])),
("viewer", user(&[("acme", Role::Viewer)], false)),
("admin", user(&[("acme", Role::Admin)], false)),
("member", user(&[("acme", Role::Member)], false)),
("beta", user(&[("beta", Role::Owner)], false)),
("root", user(&[], true)),
]);
let authn: crate::server::mcp::Authn = Arc::new(move |req, _| {
let Some(a) = req.header("authorization") else {
return Authenticated::None;
};
match callers.get(a.trim_start_matches("Bearer ")) {
Some(Caller::User { principal }) => Authenticated::User(principal.clone()),
_ => Authenticated::Refused,
}
});
let ep = Endpoint {
registry: Arc::new(r),
policy: ToolPolicy::default(),
access: None,
healthz: Arc::new(|| (true, json!({}))),
routes: None,
public_routes: None,
hooks: Hooks {
authn: Some(authn),
authorize: Some(Arc::new(|c, tool, args, scope| {
super::super::authorize_class(
c,
&tool.name,
class_for(tool, &args),
args,
scope,
false,
)
})),
events: None,
terminal: None,
ssh: None,
audit: Some(hook(log.clone(), false)),
..Default::default()
},
};
T { ep, log, _dir: dir }
}
impl T {
fn rest(&self, who: Option<&str>, tool: &str, args: Value) -> u16 {
let mut headers = vec![("User-Agent".to_string(), "t/1".to_string())];
if let Some(w) = who {
headers.push(("Authorization".into(), format!("Bearer {w}")));
}
let peer = match who {
Some(_) => Peer::Tcp("127.0.0.1:5000".parse().unwrap()),
None => Peer::Unix { uid: Some(1000) },
};
let r = self.ep.handle(&crate::server::http::Request {
method: "POST".into(),
path: format!("/api/v1/tools/{tool}"),
query: None,
headers,
body: serde_json::to_vec(&args).unwrap(),
peer,
});
r.status
}
fn mcp(&self, who: &str, tool: &str, args: Value) -> Value {
let body = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": tool, "arguments": args}});
let r = self.ep.handle(&crate::server::http::Request {
method: "POST".into(),
path: "/mcp".into(),
query: None,
headers: vec![("Authorization".into(), format!("Bearer {who}"))],
body: serde_json::to_vec(&body).unwrap(),
peer: Peer::Tcp("127.0.0.1:5000".parse().unwrap()),
});
serde_json::from_slice(&r.body).unwrap()
}
fn new_rows(&self, after: i64) -> Vec<crate::audit::Entry> {
self.log
.list(
&Query {
after: Some(after),
..Default::default()
},
&Visibility::All,
)
.unwrap()
}
fn one(&self, f: impl FnOnce()) -> crate::audit::Entry {
let h = self.log.head().unwrap();
f();
let mut rows = self.new_rows(h);
assert_eq!(rows.len(), 1, "{rows:#?}");
rows.remove(0)
}
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "predates the lint ratchet; split it when next changed"
)]
fn tool_calls_are_audited_without_values() {
let t = endpoint();
let e = t.one(|| {
let st = t.rest(
Some("agent"),
"secret_set",
json!({"org": "acme", "name": "DB_PASSWORD", "value": SECRET}),
);
assert_eq!(st, 200);
});
assert_eq!(
(e.action.as_str(), e.org.as_deref(), e.target.as_deref()),
("secret_set", Some("acme"), Some("DB_PASSWORD"))
);
assert_eq!(
(e.actor.as_str(), e.actor_kind.as_str(), e.surface.as_str()),
("u@x.io", "agent", "rest")
);
assert_eq!(
(e.user_id, e.token_id, e.token_name.as_deref()),
(Some(7), Some(3), Some("ci"))
);
assert_eq!(
(e.outcome.as_str(), e.user_agent.as_deref()),
("ok", Some("t/1"))
);
assert!(e.request_id.is_some());
assert_eq!(e.details, json!({"org": "acme", "name": "DB_PASSWORD"}));
let e = t.one(|| {
let r = t.mcp(
"agent",
"stack_deploy",
json!({
"org": "acme", "name": "web",
"compose": format!("services: {{web: {{environment: {{P: {SECRET}}}}}}}"),
"secrets": {"db": SECRET}, "vars": {"X": SECRET},
"env": {"K": SECRET}, "argv": ["echo", SECRET], "stdin": SECRET,
"service": SECRET.repeat(10),
}),
);
assert_eq!(r["result"]["isError"], false);
});
assert_eq!(
(e.action.as_str(), e.surface.as_str()),
("stack_deploy", "mcp")
);
assert_eq!(e.details, json!({"org": "acme", "name": "web"}));
let e = t.one(|| {
let st = t.rest(
Some("member"),
"secret_get",
json!({"org": "acme", "name": "DB_PASSWORD"}),
);
assert_eq!(st, 200);
});
assert_eq!(
(e.action.as_str(), e.actor_kind.as_str()),
("secret_get", "person")
);
let h = t.log.head().unwrap();
let st = t.rest(
Some("member"),
"stack_status",
json!({"org": "acme", "name": "web"}),
);
assert_eq!(st, 200);
assert!(t.new_rows(h).is_empty());
for (who, tool) in [
("viewer", "stack_deploy"),
("viewer", "secret_get"),
("ro", "sandbox_exec"),
("beta", "secret_set"),
] {
let e = t.one(|| {
let st = t.rest(
Some(who),
tool,
json!({"org": "acme", "name": "x", "value": SECRET}),
);
assert_eq!(st, 403);
});
assert_eq!(
(e.action.as_str(), e.outcome.as_str()),
(tool, "forbidden"),
"{who}"
);
}
let e = t.one(|| {
let r = t.ep.handle(&crate::server::http::Request {
method: "POST".into(),
path: "/orgs/acme/api/v1/tools/secret_get".into(),
query: None,
headers: vec![("Authorization".into(), "Bearer viewer".into())],
body: br#"{"name": "DB_PASSWORD"}"#.to_vec(),
peer: Peer::Tcp("127.0.0.1:5000".parse().unwrap()),
});
assert_eq!(r.status, 403);
});
assert_eq!(
(e.org.as_deref(), e.outcome.as_str()),
(Some("acme"), "forbidden")
);
let e = t.one(|| {
let st = t.rest(None, "stack_deploy", json!({"org": "acme", "name": "api"}));
assert_eq!(st, 200);
});
assert_eq!(
(e.actor.as_str(), e.actor_kind.as_str(), e.surface.as_str()),
("local(uid 1000)", "local", "cli")
);
assert!(t.log.verify().unwrap().ok);
let p = t.log.path().unwrap().to_path_buf();
let mut bytes = std::fs::read(&p).unwrap();
if let Ok(w) = std::fs::read(p.with_extension("db-wal")) {
bytes.extend(w);
}
for needle in [SECRET, "c2VjcmV0", "hunter2"] {
assert!(
!bytes.windows(needle.len()).any(|w| w == needle.as_bytes()),
"{needle} leaked"
);
}
}
#[test]
fn audit_list_shows_each_reader_their_share() {
let t = endpoint();
t.rest(
Some("agent"),
"secret_set",
json!({"org": "acme", "name": "a"}),
);
t.rest(
Some("beta"),
"secret_set",
json!({"org": "beta", "name": "b"}),
);
t.log
.append(NewEntry {
actor: Actor::claimed("x@y.io"),
action: "auth.login".into(),
outcome: "invalid_credentials".into(),
..Default::default()
})
.unwrap();
let list = |who: &str, args: Value| t.mcp(who, "audit_list", args)["result"].clone();
let orgs = |v: &Value| -> Vec<Option<String>> {
v["structuredContent"]["entries"]
.as_array()
.unwrap()
.iter()
.map(|e| e["org"].as_str().map(String::from))
.collect()
};
let acme = vec![Some("acme".to_string())];
assert_eq!(orgs(&list("admin", json!({}))), acme);
assert_eq!(orgs(&list("admin", json!({"org": "acme"}))), acme);
assert_eq!(list("admin", json!({"org": "beta"}))["isError"], true);
assert_eq!(list("admin", json!({"platform": true}))["isError"], true);
for who in ["member", "viewer", "agent"] {
let r = list(who, json!({"org": "acme"}));
assert_eq!(r["isError"], true, "{who}");
}
let all = orgs(&list("root", json!({})));
assert!(
all.contains(&None) && all.contains(&Some("beta".into())),
"{all:?}"
);
assert_eq!(orgs(&list("root", json!({"platform": true}))), [None]);
let r = t.mcp("admin", "audit_verify", json!({}));
assert_eq!(r["result"]["isError"], true);
let v = t.mcp("root", "audit_verify", json!({}));
assert_eq!(v["result"]["structuredContent"]["ok"], true);
}
#[test]
fn ssh_sessions_are_recorded_with_user_and_key() {
use super::super::tests::token;
let c = token(&[("acme", Role::Member)], &[]);
let args = json!({
"org": "acme", "name": "box", "ssh_user": "dev",
"fingerprint": "SHA256:OGav3hSvMQSOfHiDB0OdyYFOPHDbUJTzSsNvCdLadvQ",
"duration_s": 42,
});
let origin = Origin::default();
for action in ["ssh.open", "ssh.close"] {
let e = entry(
&Audited {
caller: &c,
action,
tool: None,
args: &args,
outcome: Ok(()),
origin: &origin,
},
false,
)
.expect("SSH sessions are always recorded");
assert_eq!(e.org.as_deref(), Some("acme"));
assert_eq!(e.target.as_deref(), Some("box"));
assert_eq!(e.details["ssh_user"], "dev");
assert_eq!(e.details["duration_s"], 42);
assert!(
e.details["fingerprint"]
.as_str()
.unwrap()
.starts_with("SHA256:")
);
}
}