1use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum AuthorizationMode {
8 BoundaryOnly,
10 PolicyEnforced,
12}
13
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct DataAccessPolicy {
18 pub allowed_labels: Vec<String>,
24 pub denied_rel_types: Vec<String>,
26 pub visible_properties: BTreeMap<String, Vec<String>>,
29 pub max_results: u64,
31}
32
33pub const ALL_LABELS: &str = "*";
35
36impl DataAccessPolicy {
37 #[must_use]
39 pub fn allows_every_label(&self) -> bool {
40 self.allowed_labels
41 .iter()
42 .any(|allowed| allowed == ALL_LABELS)
43 }
44
45 #[must_use]
49 pub fn allows_label(&self, label: &str) -> bool {
50 let label = crate::value::normalize_name(label);
51 self.allowed_labels
52 .iter()
53 .any(|allowed| allowed == ALL_LABELS || crate::value::normalize_name(allowed) == label)
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct ActivePolicy {
61 pub generation: u64,
63 pub policy: DataAccessPolicy,
65 pub mode: AuthorizationMode,
67 pub fingerprint: u64,
69 #[serde(default, skip_serializing_if = "is_zero")]
74 pub min_count: u64,
75}
76
77#[expect(
78 clippy::trivially_copy_pass_by_ref,
79 reason = "serde's skip_serializing_if passes a reference"
80)]
81fn is_zero(value: &u64) -> bool {
82 *value == 0
83}
84
85#[must_use]
92pub fn policy_fingerprint(policy: &DataAccessPolicy, mode: AuthorizationMode) -> u64 {
93 let json = serde_json::json!({
94 "labels": policy.allowed_labels, "denied": policy.denied_rel_types,
95 "visible": policy.visible_properties, "max": policy.max_results,
96 "mode": format!("{mode:?}"),
97 });
98 crate::hash::stable_hash64(&serde_json::to_vec(&json).expect("policy JSON is serializable"))
99}
100
101#[must_use]
109pub fn policy_fingerprint_with_min_count(
110 policy: &DataAccessPolicy,
111 mode: AuthorizationMode,
112 min_count: u64,
113) -> u64 {
114 if min_count == 0 {
115 return policy_fingerprint(policy, mode);
116 }
117 let json = serde_json::json!({
118 "labels": policy.allowed_labels, "denied": policy.denied_rel_types,
119 "visible": policy.visible_properties, "max": policy.max_results,
120 "mode": format!("{mode:?}"), "minCount": min_count,
121 });
122 crate::hash::stable_hash64(&serde_json::to_vec(&json).expect("policy JSON is serializable"))
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn a_threshold_changes_the_fingerprint_only_when_set() {
131 let policy = DataAccessPolicy {
132 allowed_labels: vec!["A".into()],
133 ..DataAccessPolicy::default()
134 };
135 let mode = AuthorizationMode::PolicyEnforced;
136 assert_eq!(
137 policy_fingerprint_with_min_count(&policy, mode, 0),
138 policy_fingerprint(&policy, mode)
139 );
140 assert_ne!(
141 policy_fingerprint_with_min_count(&policy, mode, 5),
142 policy_fingerprint(&policy, mode)
143 );
144 let active = ActivePolicy {
146 generation: 1,
147 policy,
148 mode,
149 fingerprint: 7,
150 min_count: 0,
151 };
152 let json = serde_json::to_value(&active).unwrap();
153 assert!(json.get("min_count").is_none());
154 let back: ActivePolicy = serde_json::from_value(json).unwrap();
155 assert_eq!(back.min_count, 0);
156 }
157
158 #[test]
159 fn the_allow_list_is_empty_for_none_star_for_all_and_ignores_case() {
160 let policy = |labels: &[&str]| DataAccessPolicy {
161 allowed_labels: labels.iter().map(ToString::to_string).collect(),
162 ..DataAccessPolicy::default()
163 };
164 assert!(!policy(&[]).allows_label("Note"));
165 assert!(!policy(&[]).allows_every_label());
166 assert!(policy(&["*"]).allows_label("Note"));
167 assert!(policy(&["Note", "*"]).allows_every_label());
168 assert!(policy(&["note"]).allows_label("Note"));
169 assert!(policy(&["first-name"]).allows_label("First_Name"));
170 assert!(!policy(&["Note"]).allows_label("Notes"));
171 assert!(!policy(&["Note"]).allows_every_label());
172 }
173}