use serde::{Deserialize, Serialize};
use serde_json::Value;
pub use super::contract::{HookCall, HookRequest};
pub const HOOK_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "decision", rename_all = "snake_case")]
pub enum HookResponse {
Allow {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
Deny {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
Modify {
input: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_bare_decision_parses() {
let allow: HookResponse = serde_json::from_str(r#"{"decision":"allow"}"#).expect("parses");
let deny: HookResponse =
serde_json::from_str(r#"{"decision":"deny","reason":"no"}"#).expect("parses");
assert_eq!(allow, HookResponse::Allow { reason: None });
assert_eq!(
deny,
HookResponse::Deny {
reason: Some("no".to_string())
}
);
}
#[test]
fn unknown_fields_do_not_break_an_older_lan() {
let response: HookResponse =
serde_json::from_str(r#"{"decision":"allow","invented_later":true}"#)
.expect("a hook may say more than basis reads");
assert_eq!(response, HookResponse::Allow { reason: None });
}
#[test]
fn a_modify_carries_the_replacement_input() {
let response: HookResponse = serde_json::from_str(
r#"{"decision":"modify","input":{"command":"ls"},"reason":"narrowed"}"#,
)
.expect("parses");
assert_eq!(
response,
HookResponse::Modify {
input: json!({"command": "ls"}),
reason: Some("narrowed".to_string()),
}
);
}
#[test]
fn a_modify_without_an_input_is_not_a_decision() {
assert!(
serde_json::from_str::<HookResponse>(r#"{"decision":"modify"}"#).is_err(),
"a hook that meant to intervene and did not say how must reach the failure path"
);
}
#[test]
fn a_decision_lan_does_not_know_is_not_silently_an_allow() {
assert!(
serde_json::from_str::<HookResponse>(r#"{"decision":"maybe"}"#).is_err(),
"an unreadable answer must reach the failure path, not the allow path"
);
}
#[test]
fn responses_round_trip() {
let deny = HookResponse::Deny {
reason: Some("because".to_string()),
};
let text = serde_json::to_string(&deny).expect("serializes");
let back: HookResponse = serde_json::from_str(&text).expect("deserializes");
assert_eq!(deny, back);
}
}