1use crate::*;
10use bloomfilter::Bloom;
11use std::hash::{Hash, Hasher};
12use std::sync::RwLock;
13
14#[derive(Debug, Clone)]
16pub struct BloomConfig {
17 pub expected_items: usize,
19 pub false_positive_rate: f64,
21}
22
23impl Default for BloomConfig {
24 fn default() -> Self {
25 Self {
26 expected_items: 1_000_000, false_positive_rate: 0.01, }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34struct BloomKey {
35 namespace: String,
36 object_id: String,
37 relation: String,
38 subject: String,
39}
40
41impl Hash for BloomKey {
42 fn hash<H: Hasher>(&self, state: &mut H) {
43 self.namespace.hash(state);
44 self.object_id.hash(state);
45 self.relation.hash(state);
46 self.subject.hash(state);
47 }
48}
49
50impl BloomKey {
51 fn new(namespace: &str, object_id: &str, relation: &str, subject: &Subject) -> Self {
52 Self {
53 namespace: namespace.to_string(),
54 object_id: object_id.to_string(),
55 relation: relation.to_string(),
56 subject: subject.to_string(),
57 }
58 }
59
60 fn from_tuple(tuple: &RelationTuple) -> Self {
61 Self::new(
62 &tuple.namespace,
63 &tuple.object_id,
64 &tuple.relation,
65 &tuple.subject,
66 )
67 }
68
69 fn from_check_request(request: &CheckRequest) -> Self {
70 Self::new(
71 &request.namespace,
72 &request.object_id,
73 &request.relation,
74 &request.subject,
75 )
76 }
77}
78
79pub struct AuthzBloomFilter {
81 filter: RwLock<Bloom<BloomKey>>,
83 items_count: RwLock<usize>,
85 config: BloomConfig,
87}
88
89impl AuthzBloomFilter {
90 pub fn new() -> Self {
92 Self::with_config(BloomConfig::default())
93 }
94
95 pub fn with_config(config: BloomConfig) -> Self {
97 let filter = Bloom::new_for_fp_rate(config.expected_items, config.false_positive_rate)
98 .expect("Failed to create bloom filter with given parameters");
99 Self {
100 filter: RwLock::new(filter),
101 items_count: RwLock::new(0),
102 config,
103 }
104 }
105
106 pub fn add_tuple(&self, tuple: &RelationTuple) {
108 let key = BloomKey::from_tuple(tuple);
109 let mut filter = self.filter.write().unwrap();
110 filter.set(&key);
111 let mut count = self.items_count.write().unwrap();
112 *count += 1;
113 }
114
115 pub fn might_contain(&self, request: &CheckRequest) -> bool {
117 let key = BloomKey::from_check_request(request);
118 let filter = self.filter.read().unwrap();
119 filter.check(&key)
120 }
121
122 pub fn might_contain_batch(&self, requests: &[CheckRequest]) -> Vec<bool> {
125 let filter = self.filter.read().unwrap();
126 requests
127 .iter()
128 .map(|req| {
129 let key = BloomKey::from_check_request(req);
130 filter.check(&key)
131 })
132 .collect()
133 }
134
135 pub fn item_count(&self) -> usize {
137 *self.items_count.read().unwrap()
138 }
139
140 pub fn clear(&self) {
142 let new_filter =
143 Bloom::new_for_fp_rate(self.config.expected_items, self.config.false_positive_rate)
144 .expect("Failed to create bloom filter with given parameters");
145 *self.filter.write().unwrap() = new_filter;
146 *self.items_count.write().unwrap() = 0;
147 }
148
149 pub fn estimated_fp_rate(&self) -> f64 {
151 let count = *self.items_count.read().unwrap();
152 if count == 0 {
153 return 0.0;
154 }
155
156 let fill_ratio = count as f64 / self.config.expected_items as f64;
158 if fill_ratio >= 1.0 {
159 return 0.5;
161 }
162
163 self.config.false_positive_rate * (1.0 + fill_ratio)
165 }
166}
167
168impl Default for AuthzBloomFilter {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174#[derive(Debug, Clone, Default)]
176pub struct BloomStats {
177 pub definite_negatives: u64,
179 pub potential_positives: u64,
181 pub true_positives: u64,
183 pub false_positives: u64,
185}
186
187impl BloomStats {
188 pub fn query_reduction_rate(&self) -> f64 {
190 let total = self.definite_negatives + self.potential_positives;
191 if total == 0 {
192 return 0.0;
193 }
194 self.definite_negatives as f64 / total as f64
195 }
196
197 pub fn actual_fp_rate(&self) -> f64 {
199 let db_checks = self.true_positives + self.false_positives;
200 if db_checks == 0 {
201 return 0.0;
202 }
203 self.false_positives as f64 / db_checks as f64
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_bloom_filter_basic() {
213 let bloom = AuthzBloomFilter::with_config(BloomConfig {
214 expected_items: 1000,
215 false_positive_rate: 0.01,
216 });
217
218 let tuple = RelationTuple::new(
220 "document",
221 "viewer",
222 "doc123",
223 Subject::User("alice".to_string()),
224 );
225 bloom.add_tuple(&tuple);
226
227 let request = CheckRequest {
229 namespace: "document".to_string(),
230 object_id: "doc123".to_string(),
231 relation: "viewer".to_string(),
232 subject: Subject::User("alice".to_string()),
233 context: None,
234 };
235 assert!(bloom.might_contain(&request));
236
237 let request2 = CheckRequest {
239 namespace: "document".to_string(),
240 object_id: "doc999".to_string(),
241 relation: "owner".to_string(),
242 subject: Subject::User("bob".to_string()),
243 context: None,
244 };
245 let _result = bloom.might_contain(&request2);
248 }
249
250 #[test]
251 fn test_bloom_filter_batch() {
252 let bloom = AuthzBloomFilter::with_config(BloomConfig {
253 expected_items: 1000,
254 false_positive_rate: 0.01,
255 });
256
257 for i in 0..10 {
259 let tuple = RelationTuple::new(
260 "document",
261 "viewer",
262 format!("doc{}", i),
263 Subject::User("alice".to_string()),
264 );
265 bloom.add_tuple(&tuple);
266 }
267
268 let requests: Vec<_> = (0..15)
270 .map(|i| CheckRequest {
271 namespace: "document".to_string(),
272 object_id: format!("doc{}", i),
273 relation: "viewer".to_string(),
274 subject: Subject::User("alice".to_string()),
275 context: None,
276 })
277 .collect();
278
279 let results = bloom.might_contain_batch(&requests);
280 assert_eq!(results.len(), 15);
281
282 for result in results.iter().take(10) {
284 assert!(*result);
285 }
286 }
287
288 #[test]
289 fn test_bloom_stats() {
290 let stats = BloomStats {
291 definite_negatives: 500,
292 potential_positives: 500,
293 true_positives: 450,
294 false_positives: 50,
295 };
296
297 assert!((stats.query_reduction_rate() - 0.5).abs() < 0.001);
298 assert!((stats.actual_fp_rate() - 0.1).abs() < 0.001);
299 }
300
301 #[test]
302 fn test_bloom_clear() {
303 let bloom = AuthzBloomFilter::with_config(BloomConfig {
304 expected_items: 1000,
305 false_positive_rate: 0.01,
306 });
307
308 let tuple = RelationTuple::new(
310 "document",
311 "viewer",
312 "doc123",
313 Subject::User("alice".to_string()),
314 );
315 bloom.add_tuple(&tuple);
316 assert_eq!(bloom.item_count(), 1);
317
318 bloom.clear();
320 assert_eq!(bloom.item_count(), 0);
321 }
322}