ricecoder_permissions/audit/
query.rs

1//! Audit log querying and filtering
2
3use super::models::{AuditAction, AuditLogEntry, AuditResult};
4use chrono::{DateTime, Utc};
5
6/// Filter criteria for audit log queries
7#[derive(Debug, Clone)]
8pub struct QueryFilter {
9    /// Filter by tool name (optional)
10    pub tool: Option<String>,
11    /// Filter by action (optional)
12    pub action: Option<AuditAction>,
13    /// Filter by result (optional)
14    pub result: Option<AuditResult>,
15    /// Filter by agent (optional)
16    pub agent: Option<String>,
17    /// Filter by start date (optional)
18    pub start_date: Option<DateTime<Utc>>,
19    /// Filter by end date (optional)
20    pub end_date: Option<DateTime<Utc>>,
21}
22
23impl QueryFilter {
24    /// Create a new empty filter
25    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    /// Filter by tool name
37    pub fn with_tool(mut self, tool: String) -> Self {
38        self.tool = Some(tool);
39        self
40    }
41
42    /// Filter by action
43    pub fn with_action(mut self, action: AuditAction) -> Self {
44        self.action = Some(action);
45        self
46    }
47
48    /// Filter by result
49    pub fn with_result(mut self, result: AuditResult) -> Self {
50        self.result = Some(result);
51        self
52    }
53
54    /// Filter by agent
55    pub fn with_agent(mut self, agent: String) -> Self {
56        self.agent = Some(agent);
57        self
58    }
59
60    /// Filter by start date
61    pub fn with_start_date(mut self, date: DateTime<Utc>) -> Self {
62        self.start_date = Some(date);
63        self
64    }
65
66    /// Filter by end date
67    pub fn with_end_date(mut self, date: DateTime<Utc>) -> Self {
68        self.end_date = Some(date);
69        self
70    }
71
72    /// Check if an entry matches this filter
73    fn matches(&self, entry: &AuditLogEntry) -> bool {
74        // Check tool filter
75        if let Some(ref tool) = self.tool {
76            if entry.tool != *tool {
77                return false;
78            }
79        }
80
81        // Check action filter
82        if let Some(action) = self.action {
83            if entry.action != action {
84                return false;
85            }
86        }
87
88        // Check result filter
89        if let Some(result) = self.result {
90            if entry.result != result {
91                return false;
92            }
93        }
94
95        // Check agent filter
96        if let Some(ref agent) = self.agent {
97            if entry.agent.as_ref() != Some(agent) {
98                return false;
99            }
100        }
101
102        // Check start date filter
103        if let Some(start_date) = self.start_date {
104            if entry.timestamp < start_date {
105                return false;
106            }
107        }
108
109        // Check end date filter
110        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/// Pagination parameters
127#[derive(Debug, Clone)]
128pub struct Pagination {
129    /// Number of results per page
130    pub limit: usize,
131    /// Number of results to skip
132    pub offset: usize,
133}
134
135impl Pagination {
136    /// Create a new pagination with limit and offset
137    pub fn new(limit: usize, offset: usize) -> Self {
138        Self { limit, offset }
139    }
140
141    /// Create pagination for the first page
142    pub fn first_page(limit: usize) -> Self {
143        Self { limit, offset: 0 }
144    }
145
146    /// Get the next page pagination
147    pub fn next_page(&self) -> Self {
148        Self {
149            limit: self.limit,
150            offset: self.offset + self.limit,
151        }
152    }
153
154    /// Get the previous page pagination
155    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/// Query result with pagination metadata
174#[derive(Debug, Clone)]
175pub struct AuditQuery {
176    /// Filtered and paginated entries
177    pub entries: Vec<AuditLogEntry>,
178    /// Total number of entries matching the filter
179    pub total: usize,
180    /// Current pagination
181    pub pagination: Pagination,
182}
183
184impl AuditQuery {
185    /// Execute a query on the given entries
186    pub fn execute(
187        entries: &[AuditLogEntry],
188        filter: &QueryFilter,
189        pagination: &Pagination,
190    ) -> Self {
191        // Filter entries
192        let filtered: Vec<_> = entries
193            .iter()
194            .filter(|e| filter.matches(e))
195            .cloned()
196            .collect();
197
198        let total = filtered.len();
199
200        // Apply pagination
201        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    /// Get the total number of pages
218    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    /// Get the current page number (1-indexed)
226    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    /// Check if there is a next page
234    pub fn has_next_page(&self) -> bool {
235        self.pagination.offset + self.pagination.limit < self.total
236    }
237
238    /// Check if there is a previous page
239    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        // All entries should be within the date range
445        assert_eq!(result.total, entries.len());
446    }
447}