greentic_types/
policy.rs

1//! Network policy primitives.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6#[cfg(feature = "schemars")]
7use schemars::JsonSchema;
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11/// Network protocols supported by allow lists.
12#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14#[cfg_attr(feature = "schemars", derive(JsonSchema))]
15#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
16pub enum Protocol {
17    /// Hypertext Transfer Protocol.
18    Http,
19    /// Hypertext Transfer Protocol Secure.
20    Https,
21    /// Generic TCP connectivity.
22    Tcp,
23    /// Generic UDP connectivity.
24    Udp,
25    /// gRPC.
26    Grpc,
27    /// Any protocol not covered above.
28    Custom(String),
29}
30
31/// Allow list describing permitted domains, ports, and protocols.
32#[derive(Clone, Debug, PartialEq, Eq)]
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34#[cfg_attr(feature = "schemars", derive(JsonSchema))]
35pub struct AllowList {
36    /// Allowed domain suffixes or exact hosts.
37    #[cfg_attr(
38        feature = "serde",
39        serde(default, skip_serializing_if = "Vec::is_empty")
40    )]
41    pub domains: Vec<String>,
42    /// Allowed port numbers.
43    #[cfg_attr(
44        feature = "serde",
45        serde(default, skip_serializing_if = "Vec::is_empty")
46    )]
47    pub ports: Vec<u16>,
48    /// Allowed network protocols.
49    #[cfg_attr(
50        feature = "serde",
51        serde(default, skip_serializing_if = "Vec::is_empty")
52    )]
53    pub protocols: Vec<Protocol>,
54}
55
56impl AllowList {
57    /// Creates an empty allow list.
58    pub fn empty() -> Self {
59        Self {
60            domains: Vec::new(),
61            ports: Vec::new(),
62            protocols: Vec::new(),
63        }
64    }
65
66    /// Returns `true` when the allow list contains no rules.
67    pub fn is_empty(&self) -> bool {
68        self.domains.is_empty() && self.ports.is_empty() && self.protocols.is_empty()
69    }
70}
71
72impl Default for AllowList {
73    fn default() -> Self {
74        Self::empty()
75    }
76}
77
78/// High-level network policy composed of allow lists.
79#[derive(Clone, Debug, PartialEq, Eq, Default)]
80#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
81#[cfg_attr(feature = "schemars", derive(JsonSchema))]
82pub struct NetworkPolicy {
83    /// Allow list enforced for egress connections.
84    pub egress: AllowList,
85    /// Whether destinations not present in the allow list should be denied.
86    pub deny_on_miss: bool,
87}
88
89impl NetworkPolicy {
90    /// Creates a policy denying unknown destinations by default.
91    pub fn strict(egress: AllowList) -> Self {
92        Self {
93            egress,
94            deny_on_miss: true,
95        }
96    }
97}
98
99/// Result of evaluating a network policy.
100#[derive(Clone, Debug, PartialEq, Eq)]
101#[cfg_attr(feature = "schemars", derive(JsonSchema))]
102pub struct PolicyDecision {
103    /// Canonical status for the decision.
104    pub status: PolicyDecisionStatus,
105    /// Optional list of reasons.
106    pub reasons: Vec<String>,
107    /// Legacy allow flag (retained for backward compatibility).
108    pub allow: Option<bool>,
109    /// Legacy single reason (retained for backward compatibility).
110    pub reason: Option<String>,
111}
112
113/// Status for a policy decision.
114#[derive(Clone, Debug, PartialEq, Eq, Hash)]
115#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
116#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
117#[cfg_attr(feature = "schemars", derive(JsonSchema))]
118pub enum PolicyDecisionStatus {
119    /// Request is allowed.
120    Allow,
121    /// Request is denied.
122    Deny,
123}
124
125#[cfg(feature = "serde")]
126mod serde_impls {
127    use super::{PolicyDecision, PolicyDecisionStatus};
128    use alloc::vec::Vec;
129    use serde::de::{self, MapAccess, Visitor};
130    use serde::ser::SerializeStruct;
131    use serde::{Deserialize, Deserializer, Serialize, Serializer};
132
133    impl Serialize for PolicyDecision {
134        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135        where
136            S: Serializer,
137        {
138            // status + reasons always emitted; legacy fields only when present.
139            let mut len = 2;
140            if self.allow.is_some() {
141                len += 1;
142            }
143            if self.reason.is_some() {
144                len += 1;
145            }
146            let mut state = serializer.serialize_struct("PolicyDecision", len)?;
147            state.serialize_field("status", &self.status)?;
148            state.serialize_field("reasons", &self.reasons)?;
149            if let Some(allow) = &self.allow {
150                state.serialize_field("allow", allow)?;
151            }
152            if let Some(reason) = &self.reason {
153                state.serialize_field("reason", reason)?;
154            }
155            state.end()
156        }
157    }
158
159    impl<'de> Deserialize<'de> for PolicyDecision {
160        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161        where
162            D: Deserializer<'de>,
163        {
164            enum Field {
165                Allow,
166                Reason,
167                Status,
168                Reasons,
169                Unknown,
170            }
171
172            impl<'de> Deserialize<'de> for Field {
173                fn deserialize<D2>(deserializer: D2) -> Result<Self, D2::Error>
174                where
175                    D2: Deserializer<'de>,
176                {
177                    struct FieldVisitor;
178
179                    impl<'de> Visitor<'de> for FieldVisitor {
180                        type Value = Field;
181
182                        fn expecting(
183                            &self,
184                            formatter: &mut core::fmt::Formatter,
185                        ) -> core::fmt::Result {
186                            formatter.write_str("`allow`, `reason`, `status`, or `reasons`")
187                        }
188
189                        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
190                        where
191                            E: de::Error,
192                        {
193                            Ok(match value {
194                                "allow" => Field::Allow,
195                                "reason" => Field::Reason,
196                                "status" => Field::Status,
197                                "reasons" => Field::Reasons,
198                                _ => Field::Unknown,
199                            })
200                        }
201                    }
202
203                    deserializer.deserialize_identifier(FieldVisitor)
204                }
205            }
206
207            struct PolicyDecisionVisitor;
208
209            impl<'de> Visitor<'de> for PolicyDecisionVisitor {
210                type Value = PolicyDecision;
211
212                fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
213                    formatter.write_str("policy decision")
214                }
215
216                fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
217                where
218                    M: MapAccess<'de>,
219                {
220                    let mut allow: Option<Option<bool>> = None;
221                    let mut reason: Option<Option<String>> = None;
222                    let mut status: Option<PolicyDecisionStatus> = None;
223                    let mut reasons: Option<Vec<String>> = None;
224
225                    while let Some(key) = map.next_key()? {
226                        match key {
227                            Field::Allow => {
228                                if allow.is_some() {
229                                    return Err(de::Error::duplicate_field("allow"));
230                                }
231                                allow = Some(map.next_value()?);
232                            }
233                            Field::Reason => {
234                                if reason.is_some() {
235                                    return Err(de::Error::duplicate_field("reason"));
236                                }
237                                reason = Some(map.next_value()?);
238                            }
239                            Field::Status => {
240                                if status.is_some() {
241                                    return Err(de::Error::duplicate_field("status"));
242                                }
243                                status = Some(map.next_value()?);
244                            }
245                            Field::Reasons => {
246                                if reasons.is_some() {
247                                    return Err(de::Error::duplicate_field("reasons"));
248                                }
249                                reasons = Some(map.next_value()?);
250                            }
251                            Field::Unknown => {
252                                // Ignore unknown fields for forward compatibility.
253                                let _ = map.next_value::<de::IgnoredAny>()?;
254                            }
255                        }
256                    }
257
258                    let status = status
259                        .or_else(|| {
260                            allow.flatten().map(|flag| {
261                                if flag {
262                                    PolicyDecisionStatus::Allow
263                                } else {
264                                    PolicyDecisionStatus::Deny
265                                }
266                            })
267                        })
268                        .unwrap_or(PolicyDecisionStatus::Allow);
269
270                    let reasons_vec = match (reasons, reason.clone()) {
271                        (Some(list), _) => list,
272                        (None, Some(Some(msg))) => alloc::vec![msg],
273                        (None, _) => Vec::new(),
274                    };
275
276                    Ok(PolicyDecision {
277                        status,
278                        reasons: reasons_vec,
279                        allow: allow.flatten(),
280                        reason: reason.flatten(),
281                    })
282                }
283            }
284
285            deserializer.deserialize_struct(
286                "PolicyDecision",
287                &["status", "reasons", "allow", "reason"],
288                PolicyDecisionVisitor,
289            )
290        }
291    }
292}