aauth_protocol/mission.rs
1//! Missions (AAuth §Mission): what a person approved an agent to do. A
2//! mission is named everywhere afterwards by its `s256`, the hash of its
3//! exact bytes, so nobody can change what was approved without changing its
4//! name.
5
6use base64::engine::general_purpose::URL_SAFE_NO_PAD;
7use base64::Engine;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11/// A mission as a Person Server approved it, checked against its hash.
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13pub struct ApprovedMission {
14 /// `BASE64URL(SHA-256(mission bytes))`: the `mission_s256` later tokens carry.
15 pub s256: String,
16 /// The decoded mission document.
17 pub mission: Value,
18}
19
20impl ApprovedMission {
21 /// The mission an approval body carries (`s256`, and `mission` as
22 /// base64url bytes), when its bytes hash to the `s256` it names.
23 ///
24 /// ```
25 /// # use aauth_protocol::mission::ApprovedMission;
26 /// use base64::Engine;
27 /// let bytes = br#"{"description":"Sydney","approved_resources":["https://api.booking.example"]}"#;
28 /// let body = serde_json::json!({
29 /// "s256": aauth_protocol::s256(bytes),
30 /// "mission": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes),
31 /// });
32 /// let mission = ApprovedMission::from_approval(&body).unwrap();
33 /// assert_eq!(mission.approved_resources(), ["https://api.booking.example"]);
34 /// ```
35 pub fn from_approval(body: &Value) -> Option<Self> {
36 let s256 = body.get("s256")?.as_str()?;
37 let bytes = URL_SAFE_NO_PAD
38 .decode(body.get("mission")?.as_str()?.trim_end_matches('='))
39 .ok()?;
40 (crate::s256(&bytes) == s256).then(|| Self {
41 s256: s256.to_string(),
42 mission: serde_json::from_slice(&bytes).unwrap_or(Value::Null),
43 })
44 }
45
46 /// The resources the mission was approved for.
47 pub fn approved_resources(&self) -> Vec<String> {
48 self.mission
49 .get("approved_resources")
50 .and_then(Value::as_array)
51 .map(|list| {
52 list.iter()
53 .filter_map(|r| r.as_str().map(str::to_string))
54 .collect()
55 })
56 .unwrap_or_default()
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use serde_json::json;
64
65 #[test]
66 fn a_mission_whose_bytes_do_not_match_its_hash_is_refused() {
67 let bytes = br#"{"agent":"a"}"#;
68 let granted =
69 json!({ "s256": crate::s256(bytes), "mission": URL_SAFE_NO_PAD.encode(bytes) });
70 assert!(ApprovedMission::from_approval(&granted).is_some());
71 let mut tampered = granted.clone();
72 tampered["s256"] = json!("not-the-hash");
73 assert!(ApprovedMission::from_approval(&tampered).is_none());
74 }
75}