188 lines
5.7 KiB
Rust
188 lines
5.7 KiB
Rust
//! The four messages of `gateway.sock` against byte-exact fixtures, every object in them refusing
|
|
//! an unknown key, and the checks on an answer (M4b spec, section 4). Do not edit these or the
|
|
//! fixtures.
|
|
|
|
use proto::{
|
|
ApprovalOutcome, ApprovalPosted, ApprovalReply, ApprovalRequest, CallId, DataClass, DenyReason,
|
|
Envelope, GatewayOutcome, MAX_REASON, Message, PendingApproval, SessionId, Timestamp,
|
|
is_mattermost_id,
|
|
};
|
|
use serde_json::Value;
|
|
|
|
const BY: &str = "k0000000000000000000000000";
|
|
const POST: &str = "c0000000000000000000000001";
|
|
|
|
fn fixture(name: &str) -> String {
|
|
let path = format!("{}/tests/fixtures/wire/{name}", env!("CARGO_MANIFEST_DIR"));
|
|
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}"));
|
|
text.trim_end_matches('\n').to_string()
|
|
}
|
|
|
|
/// The fixture must decode to `want`, and `want` must encode to exactly the fixture's bytes.
|
|
fn check(name: &str, id: u64, last: bool, msg: Message) {
|
|
let want = Envelope {
|
|
v: 1,
|
|
id,
|
|
r#final: last,
|
|
msg,
|
|
};
|
|
let text = fixture(name);
|
|
let got: Envelope = serde_json::from_str(&text).unwrap_or_else(|e| panic!("{name}: {e}"));
|
|
assert_eq!(got, want, "{name}: decoded value");
|
|
assert_eq!(
|
|
serde_json::to_string(&want).unwrap(),
|
|
text,
|
|
"{name}: encoded bytes"
|
|
);
|
|
}
|
|
|
|
fn pending() -> PendingApproval {
|
|
PendingApproval {
|
|
approval: 41,
|
|
session: SessionId::new("mm-r0000000000000000000000001").unwrap(),
|
|
call: CallId(3),
|
|
tool: "shell".to_string(),
|
|
arguments: r#"{"command":"ls -la /srv"}"#.to_string(),
|
|
grant: "shell-srv".to_string(),
|
|
taint: DataClass::Private,
|
|
created: Timestamp::parse("2026-09-24T08:05:00.000Z").unwrap(),
|
|
expires: Timestamp::parse("2026-09-24T08:20:00.000Z").unwrap(),
|
|
}
|
|
}
|
|
|
|
fn reply(approved: bool, reason: Option<&str>) -> ApprovalReply {
|
|
ApprovalReply {
|
|
approval: 41,
|
|
approved,
|
|
by: BY.to_string(),
|
|
post: POST.to_string(),
|
|
reason: reason.map(str::to_string),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_four_messages() {
|
|
check(
|
|
"gateway_request.json",
|
|
1,
|
|
true,
|
|
Message::ApprovalRequest(ApprovalRequest {
|
|
approval: pending(),
|
|
}),
|
|
);
|
|
let posts = vec![
|
|
"p0000000000000000000000001".to_string(),
|
|
"p0000000000000000000000002".to_string(),
|
|
];
|
|
check(
|
|
"gateway_posted.json",
|
|
1,
|
|
false,
|
|
Message::ApprovalPosted(ApprovalPosted { posts }),
|
|
);
|
|
check(
|
|
"gateway_reply_approved.json",
|
|
1,
|
|
true,
|
|
Message::ApprovalReply(reply(true, None)),
|
|
);
|
|
check(
|
|
"gateway_reply_refused.json",
|
|
1,
|
|
true,
|
|
Message::ApprovalReply(reply(false, Some("not on a Friday"))),
|
|
);
|
|
for (name, outcome) in [
|
|
("gateway_outcome_runs.json", GatewayOutcome::Runs {}),
|
|
(
|
|
"gateway_outcome_denied.json",
|
|
GatewayOutcome::Denied {
|
|
reason: DenyReason::NoGrant,
|
|
},
|
|
),
|
|
("gateway_outcome_refused.json", GatewayOutcome::Refused {}),
|
|
("gateway_outcome_gone.json", GatewayOutcome::Gone {}),
|
|
] {
|
|
check(
|
|
name,
|
|
2,
|
|
true,
|
|
Message::ApprovalOutcome(ApprovalOutcome { outcome }),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every copy of `value` with one extra key in one object.
|
|
fn with_one_unknown_key(value: &Value) -> Vec<Value> {
|
|
let mut out = Vec::new();
|
|
if let Value::Object(map) = value {
|
|
let mut extended = map.clone();
|
|
extended.insert("zz_unknown".to_string(), Value::Bool(true));
|
|
out.push(Value::Object(extended));
|
|
for (key, child) in map {
|
|
for changed in with_one_unknown_key(child) {
|
|
let mut copy = map.clone();
|
|
copy.insert(key.clone(), changed);
|
|
out.push(Value::Object(copy));
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn every_object_refuses_an_unknown_key() {
|
|
for (name, objects) in [
|
|
("gateway_request.json", 4),
|
|
("gateway_posted.json", 3),
|
|
("gateway_reply_approved.json", 3),
|
|
("gateway_reply_refused.json", 3),
|
|
("gateway_outcome_runs.json", 4),
|
|
("gateway_outcome_denied.json", 4),
|
|
("gateway_outcome_refused.json", 4),
|
|
("gateway_outcome_gone.json", 4),
|
|
] {
|
|
let value: Value = serde_json::from_str(&fixture(name)).unwrap();
|
|
let variations = with_one_unknown_key(&value);
|
|
assert_eq!(variations.len(), objects, "{name}: objects walked");
|
|
for changed in variations {
|
|
assert!(
|
|
serde_json::from_value::<Envelope>(changed.clone()).is_err(),
|
|
"{name}: accepted {changed}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_answer_is_checked() {
|
|
assert_eq!(reply(true, None).check(), Ok(()));
|
|
assert_eq!(reply(false, Some(&"x".repeat(MAX_REASON))).check(), Ok(()));
|
|
assert_eq!(
|
|
reply(false, Some(&"é".repeat(MAX_REASON))).check(),
|
|
Ok(()),
|
|
"characters, not bytes"
|
|
);
|
|
assert!(
|
|
reply(false, Some(&"x".repeat(MAX_REASON + 1)))
|
|
.check()
|
|
.is_err()
|
|
);
|
|
for bad in [
|
|
"",
|
|
"K0000000000000000000000000",
|
|
"k000000000000000000000000",
|
|
"k00000000000000000000000000",
|
|
"../../../../../../../../etc",
|
|
] {
|
|
let mut r = reply(true, None);
|
|
r.by = bad.to_string();
|
|
assert!(r.check().is_err(), "by {bad:?}");
|
|
let mut r = reply(true, None);
|
|
r.post = bad.to_string();
|
|
assert!(r.check().is_err(), "post {bad:?}");
|
|
assert!(!is_mattermost_id(bad), "{bad:?}");
|
|
}
|
|
assert!(is_mattermost_id(BY) && is_mattermost_id(POST));
|
|
}
|