ricecoder_permissions/audit/
query.rs1use super::models::{AuditAction, AuditLogEntry, AuditResult};
4use chrono::{DateTime, Utc};
5
6#[derive(Debug, Clone)]
8pub struct QueryFilter {
9 pub tool: Option<String>,
11 pub action: Option<AuditAction>,
13 pub result: Option<AuditResult>,
15 pub agent: Option<String>,
17 pub start_date: Option<DateTime<Utc>>,
19 pub end_date: Option<DateTime<Utc>>,
21}
22
23impl QueryFilter {
24 pub fn new() -> Self {
26 Self {
27 tool: None,
28 action: None,
29 result: None,
30 agent: None,
31 start_date: None,
32 end_date: None,
33 }
34 }
35
36 pub fn with_tool(mut self, tool: String) -> Self {
38 self.tool = Some(tool);
39 self
40 }
41
42 pub fn with_action(mut self, action: AuditAction) -> Self {
44 self.action = Some(action);
45 self
46 }
47
48 pub fn with_result(mut self, result: AuditResult) -> Self {
50 self.result = Some(result);
51 self
52 }
53
54 pub fn with_agent(mut self, agent: String) -> Self {
56 self.agent = Some(agent);
57 self
58 }
59
60 pub fn with_start_date(mut self, date: DateTime<Utc>) -> Self {
62 self.start_date = Some(date);
63 self
64 }
65
66 pub fn with_end_date(mut self, date: DateTime<Utc>) -> Self {
68 self.end_date = Some(date);
69 self
70 }
71
72 fn matches(&self, entry: &AuditLogEntry) -> bool {
74 if let Some(ref tool) = self.tool {
76 if entry.tool != *tool {
77 return false;
78 }
79 }
80
81 if let Some(action) = self.action {
83 if entry.action != action {
84 return false;
85 }
86 }
87
88 if let Some(result) = self.result {
90 if entry.result != result {
91 return false;
92 }
93 }
94
95 if let Some(ref agent) = self.agent {
97 if entry.agent.as_ref() != Some(agent) {
98 return false;
99 }
100 }
101
102 if let Some(start_date) = self.start_date {
104 if entry.timestamp < start_date {
105 return false;
106 }
107 }
108
109 if let Some(end_date) = self.end_date {
111 if entry.timestamp > end_date {
112 return false;
113 }
114 }
115
116 true
117 }
118}
119
120impl Default for QueryFilter {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126#[derive(Debug, Clone)]
128pub struct Pagination {
129 pub limit: usize,
131 pub offset: usize,
133}
134
135impl Pagination {
136 pub fn new(limit: usize, offset: usize) -> Self {
138 Self { limit, offset }
139 }
140
141 pub fn first_page(limit: usize) -> Self {
143 Self { limit, offset: 0 }
144 }
145
146 pub fn next_page(&self) -> Self {
148 Self {
149 limit: self.limit,
150 offset: self.offset + self.limit,
151 }
152 }
153
154 pub fn prev_page(&self) -> Option<Self> {
156 if self.offset >= self.limit {
157 Some(Self {
158 limit: self.limit,
159 offset: self.offset - self.limit,
160 })
161 } else {
162 None
163 }
164 }
165}
166
167impl Default for Pagination {
168 fn default() -> Self {
169 Self::new(10, 0)
170 }
171}
172
173#[derive(Debug, Clone)]
175pub struct AuditQuery {
176 pub entries: Vec<AuditLogEntry>,
178 pub total: usize,
180 pub pagination: Pagination,
182}
183
184impl AuditQuery {
185 pub fn execute(
187 entries: &[AuditLogEntry],
188 filter: &QueryFilter,
189 pagination: &Pagination,
190 ) -> Self {
191 let filtered: Vec<_> = entries
193 .iter()
194 .filter(|e| filter.matches(e))
195 .cloned()
196 .collect();
197
198 let total = filtered.len();
199
200 let start = pagination.offset;
202 let end = std::cmp::min(start + pagination.limit, total);
203
204 let paginated: Vec<_> = if start < total {
205 filtered[start..end].to_vec()
206 } else {
207 Vec::new()
208 };
209
210 Self {
211 entries: paginated,
212 total,
213 pagination: pagination.clone(),
214 }
215 }
216
217 pub fn total_pages(&self) -> usize {
219 if self.pagination.limit == 0 {
220 return 0;
221 }
222 self.total.div_ceil(self.pagination.limit)
223 }
224
225 pub fn current_page(&self) -> usize {
227 if self.pagination.limit == 0 {
228 return 0;
229 }
230 (self.pagination.offset / self.pagination.limit) + 1
231 }
232
233 pub fn has_next_page(&self) -> bool {
235 self.pagination.offset + self.pagination.limit < self.total
236 }
237
238 pub fn has_prev_page(&self) -> bool {
240 self.pagination.offset > 0
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 fn create_test_entries() -> Vec<AuditLogEntry> {
249 vec![
250 AuditLogEntry::new(
251 "tool1".to_string(),
252 AuditAction::Allowed,
253 AuditResult::Success,
254 ),
255 AuditLogEntry::with_agent(
256 "tool2".to_string(),
257 AuditAction::Denied,
258 AuditResult::Blocked,
259 "agent1".to_string(),
260 ),
261 AuditLogEntry::new(
262 "tool1".to_string(),
263 AuditAction::Prompted,
264 AuditResult::Success,
265 ),
266 AuditLogEntry::with_agent(
267 "tool3".to_string(),
268 AuditAction::Allowed,
269 AuditResult::Success,
270 "agent2".to_string(),
271 ),
272 ]
273 }
274
275 #[test]
276 fn test_filter_by_tool() {
277 let entries = create_test_entries();
278 let filter = QueryFilter::new().with_tool("tool1".to_string());
279 let pagination = Pagination::first_page(10);
280
281 let result = AuditQuery::execute(&entries, &filter, &pagination);
282
283 assert_eq!(result.total, 2);
284 assert_eq!(result.entries.len(), 2);
285 assert!(result.entries.iter().all(|e| e.tool == "tool1"));
286 }
287
288 #[test]
289 fn test_filter_by_action() {
290 let entries = create_test_entries();
291 let filter = QueryFilter::new().with_action(AuditAction::Allowed);
292 let pagination = Pagination::first_page(10);
293
294 let result = AuditQuery::execute(&entries, &filter, &pagination);
295
296 assert_eq!(result.total, 2);
297 assert!(result
298 .entries
299 .iter()
300 .all(|e| e.action == AuditAction::Allowed));
301 }
302
303 #[test]
304 fn test_filter_by_result() {
305 let entries = create_test_entries();
306 let filter = QueryFilter::new().with_result(AuditResult::Success);
307 let pagination = Pagination::first_page(10);
308
309 let result = AuditQuery::execute(&entries, &filter, &pagination);
310
311 assert_eq!(result.total, 3);
312 assert!(result
313 .entries
314 .iter()
315 .all(|e| e.result == AuditResult::Success));
316 }
317
318 #[test]
319 fn test_filter_by_agent() {
320 let entries = create_test_entries();
321 let filter = QueryFilter::new().with_agent("agent1".to_string());
322 let pagination = Pagination::first_page(10);
323
324 let result = AuditQuery::execute(&entries, &filter, &pagination);
325
326 assert_eq!(result.total, 1);
327 assert_eq!(result.entries[0].agent, Some("agent1".to_string()));
328 }
329
330 #[test]
331 fn test_combined_filters() {
332 let entries = create_test_entries();
333 let filter = QueryFilter::new()
334 .with_tool("tool1".to_string())
335 .with_action(AuditAction::Allowed);
336 let pagination = Pagination::first_page(10);
337
338 let result = AuditQuery::execute(&entries, &filter, &pagination);
339
340 assert_eq!(result.total, 1);
341 assert_eq!(result.entries[0].tool, "tool1");
342 assert_eq!(result.entries[0].action, AuditAction::Allowed);
343 }
344
345 #[test]
346 fn test_pagination_first_page() {
347 let entries = create_test_entries();
348 let filter = QueryFilter::new();
349 let pagination = Pagination::first_page(2);
350
351 let result = AuditQuery::execute(&entries, &filter, &pagination);
352
353 assert_eq!(result.total, 4);
354 assert_eq!(result.entries.len(), 2);
355 assert_eq!(result.current_page(), 1);
356 assert!(result.has_next_page());
357 assert!(!result.has_prev_page());
358 }
359
360 #[test]
361 fn test_pagination_second_page() {
362 let entries = create_test_entries();
363 let filter = QueryFilter::new();
364 let pagination = Pagination::new(2, 2);
365
366 let result = AuditQuery::execute(&entries, &filter, &pagination);
367
368 assert_eq!(result.total, 4);
369 assert_eq!(result.entries.len(), 2);
370 assert_eq!(result.current_page(), 2);
371 assert!(!result.has_next_page());
372 assert!(result.has_prev_page());
373 }
374
375 #[test]
376 fn test_pagination_total_pages() {
377 let entries = create_test_entries();
378 let filter = QueryFilter::new();
379 let pagination = Pagination::first_page(2);
380
381 let result = AuditQuery::execute(&entries, &filter, &pagination);
382
383 assert_eq!(result.total_pages(), 2);
384 }
385
386 #[test]
387 fn test_pagination_offset_beyond_total() {
388 let entries = create_test_entries();
389 let filter = QueryFilter::new();
390 let pagination = Pagination::new(2, 10);
391
392 let result = AuditQuery::execute(&entries, &filter, &pagination);
393
394 assert_eq!(result.entries.len(), 0);
395 }
396
397 #[test]
398 fn test_pagination_next_page() {
399 let pagination = Pagination::first_page(2);
400 let next = pagination.next_page();
401
402 assert_eq!(next.offset, 2);
403 assert_eq!(next.limit, 2);
404 }
405
406 #[test]
407 fn test_pagination_prev_page() {
408 let pagination = Pagination::new(2, 2);
409 let prev = pagination.prev_page();
410
411 assert!(prev.is_some());
412 assert_eq!(prev.unwrap().offset, 0);
413 }
414
415 #[test]
416 fn test_pagination_prev_page_first_page() {
417 let pagination = Pagination::first_page(2);
418 let prev = pagination.prev_page();
419
420 assert!(prev.is_none());
421 }
422
423 #[test]
424 fn test_query_filter_default() {
425 let filter = QueryFilter::default();
426 let entries = create_test_entries();
427
428 assert!(entries.iter().all(|e| filter.matches(e)));
429 }
430
431 #[test]
432 fn test_date_range_filter() {
433 let entries = create_test_entries();
434 let now = Utc::now();
435 let future = now + chrono::Duration::hours(1);
436
437 let filter = QueryFilter::new()
438 .with_start_date(now - chrono::Duration::hours(1))
439 .with_end_date(future);
440 let pagination = Pagination::first_page(10);
441
442 let result = AuditQuery::execute(&entries, &filter, &pagination);
443
444 assert_eq!(result.total, entries.len());
446 }
447}