Skip to main content

bitwarden_pam/access_rules/
conditions.rs

1use serde::{Deserialize, Serialize};
2
3/// A single condition that gates access under an access rule.
4#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
5#[bitwarden_ffi::wasm_record]
6#[serde(tag = "kind", rename_all = "snake_case")]
7pub enum AccessCondition {
8    /// Requires a human approval before access is granted.
9    HumanApproval,
10    /// Restricts access to a set of allow-listed CIDR ranges.
11    IpAllowlist {
12        /// The list of allowed CIDR ranges, e.g. `10.0.0.0/8`.
13        cidrs: Vec<String>,
14    },
15    /// A kind this SDK doesn't model, or a known kind with an unexpected shape, kept verbatim so
16    /// round trips preserve it. At runtime `kind` holds the server's discriminant, not the
17    /// generated type's `"unknown"`.
18    #[serde(untagged)]
19    Unknown(
20        #[cfg_attr(feature = "wasm", tsify(type = "Record<string, unknown>"))] serde_json::Value,
21    ),
22}
23
24#[cfg(test)]
25mod tests {
26    use serde_json::json;
27
28    use super::*;
29
30    #[test]
31    fn human_approval_roundtrips() {
32        let condition = AccessCondition::HumanApproval;
33        let json = serde_json::to_value(&condition).unwrap();
34        assert_eq!(json, json!({ "kind": "human_approval" }));
35
36        let parsed: AccessCondition = serde_json::from_value(json).unwrap();
37        assert_eq!(parsed, condition);
38    }
39
40    #[test]
41    fn ip_allowlist_roundtrips() {
42        let condition = AccessCondition::IpAllowlist {
43            cidrs: vec!["10.0.0.0/8".to_string(), "2001:db8::/32".to_string()],
44        };
45        let json = serde_json::to_value(&condition).unwrap();
46        assert_eq!(
47            json,
48            json!({ "kind": "ip_allowlist", "cidrs": ["10.0.0.0/8", "2001:db8::/32"] })
49        );
50
51        let parsed: AccessCondition = serde_json::from_value(json).unwrap();
52        assert_eq!(parsed, condition);
53    }
54
55    #[test]
56    fn unknown_kind_is_preserved_verbatim() {
57        let raw = json!({
58            "kind": "time_of_day",
59            "tz": "UTC",
60            "windows": [{ "start": "09:00", "end": "17:00" }],
61        });
62
63        let parsed: AccessCondition = serde_json::from_value(raw.clone()).unwrap();
64        assert_eq!(parsed, AccessCondition::Unknown(raw.clone()));
65
66        // Compared as `Value`, not as a string, since key order isn't preserved without
67        // serde_json's `preserve_order` feature.
68        let reserialized = serde_json::to_value(&parsed).unwrap();
69        assert_eq!(reserialized, raw);
70    }
71
72    /// Serde falls back to the untagged [`AccessCondition::Unknown`] when a recognized tag has the
73    /// wrong shape, so a malformed condition survives a round trip instead of failing the list.
74    #[test]
75    fn malformed_known_kind_degrades_to_unknown() {
76        let raw = json!({ "kind": "ip_allowlist" });
77
78        let parsed: AccessCondition = serde_json::from_value(raw.clone()).unwrap();
79        assert_eq!(parsed, AccessCondition::Unknown(raw));
80    }
81
82    /// Extra fields on a modeled kind are dropped on a round trip, since serde matches the tagged
83    /// variant and never reaches the catch-all. A server-side field addition needs an SDK update.
84    #[test]
85    fn extra_fields_on_known_kind_are_dropped() {
86        let raw = json!({ "kind": "human_approval", "future_field": "not preserved" });
87        let parsed: AccessCondition = serde_json::from_value(raw).unwrap();
88        assert_eq!(parsed, AccessCondition::HumanApproval);
89        assert_eq!(
90            serde_json::to_value(&parsed).unwrap(),
91            json!({ "kind": "human_approval" })
92        );
93
94        let raw = json!({ "kind": "ip_allowlist", "cidrs": ["10.0.0.0/8"], "extra": "dropped" });
95        let parsed: AccessCondition = serde_json::from_value(raw).unwrap();
96        assert_eq!(
97            serde_json::to_value(&parsed).unwrap(),
98            json!({ "kind": "ip_allowlist", "cidrs": ["10.0.0.0/8"] })
99        );
100    }
101}