auth_framework/analytics/
dashboard.rs

1//! RBAC Analytics Dashboard
2//!
3//! This module provides dashboard components for visualizing
4//! RBAC analytics data and system performance metrics.
5
6use super::{AnalyticsError, TimeRange};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Dashboard configuration
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct DashboardConfig {
13    /// Refresh interval for real-time data
14    pub refresh_interval_seconds: u32,
15
16    /// Default time range for widgets
17    pub default_time_range_hours: u32,
18
19    /// Enable real-time updates
20    pub real_time_updates: bool,
21
22    /// Maximum number of data points per chart
23    pub max_chart_points: usize,
24
25    /// Enable alerts and notifications
26    pub alerts_enabled: bool,
27}
28
29impl Default for DashboardConfig {
30    fn default() -> Self {
31        Self {
32            refresh_interval_seconds: 30,
33            default_time_range_hours: 24,
34            real_time_updates: true,
35            max_chart_points: 100,
36            alerts_enabled: true,
37        }
38    }
39}
40
41/// Dashboard widget types
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub enum WidgetType {
44    /// Line chart for time series data
45    LineChart,
46    /// Bar chart for categorical data
47    BarChart,
48    /// Pie chart for distribution data
49    PieChart,
50    /// Single metric display
51    MetricCard,
52    /// Data table
53    Table,
54    /// Heat map
55    HeatMap,
56    /// Gauge/progress indicator
57    Gauge,
58}
59
60/// Dashboard widget configuration
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct DashboardWidget {
63    /// Widget identifier
64    pub id: String,
65
66    /// Widget title
67    pub title: String,
68
69    /// Widget type
70    pub widget_type: WidgetType,
71
72    /// Data source query
73    pub data_source: DataSource,
74
75    /// Time range for data
76    pub time_range: TimeRange,
77
78    /// Widget position and size
79    pub layout: WidgetLayout,
80
81    /// Refresh interval override
82    pub refresh_interval: Option<u32>,
83
84    /// Alert thresholds
85    pub alert_thresholds: Option<AlertThresholds>,
86}
87
88/// Widget layout configuration
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct WidgetLayout {
91    /// X position (grid units)
92    pub x: u32,
93
94    /// Y position (grid units)
95    pub y: u32,
96
97    /// Width (grid units)
98    pub width: u32,
99
100    /// Height (grid units)
101    pub height: u32,
102}
103
104/// Data source configuration for widgets
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub enum DataSource {
107    /// Role usage statistics
108    RoleUsage {
109        role_id: Option<String>,
110        group_by: Option<String>,
111    },
112    /// Permission usage statistics
113    PermissionUsage {
114        permission_id: Option<String>,
115        group_by: Option<String>,
116    },
117    /// Compliance metrics
118    Compliance { metric_type: String },
119    /// Performance metrics
120    Performance { metric_type: String },
121    /// Event count with filters
122    EventCount {
123        event_type: Option<String>,
124        filters: HashMap<String, String>,
125    },
126    /// Custom query
127    Custom {
128        query: String,
129        parameters: HashMap<String, String>,
130    },
131}
132
133/// Alert threshold configuration
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct AlertThresholds {
136    /// Warning threshold
137    pub warning: f64,
138
139    /// Critical threshold
140    pub critical: f64,
141
142    /// Threshold comparison type
143    pub comparison: ThresholdComparison,
144}
145
146/// Threshold comparison types
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub enum ThresholdComparison {
149    GreaterThan,
150    LessThan,
151    Equals,
152    NotEquals,
153}
154
155/// Dashboard data point
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct DataPoint {
158    /// Timestamp (for time series)
159    pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
160
161    /// Category label (for categorical data)
162    pub label: Option<String>,
163
164    /// Numeric value
165    pub value: f64,
166
167    /// Additional metadata
168    pub metadata: HashMap<String, String>,
169}
170
171/// Chart data series
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ChartSeries {
174    /// Series name
175    pub name: String,
176
177    /// Data points
178    pub data: Vec<DataPoint>,
179
180    /// Series color
181    pub color: Option<String>,
182
183    /// Series type (for mixed charts)
184    pub series_type: Option<String>,
185}
186
187/// Widget data response
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct WidgetData {
190    /// Widget ID
191    pub widget_id: String,
192
193    /// Last updated timestamp
194    pub updated_at: chrono::DateTime<chrono::Utc>,
195
196    /// Chart series data
197    pub series: Vec<ChartSeries>,
198
199    /// Summary statistics
200    pub summary: Option<WidgetSummary>,
201
202    /// Alert status
203    pub alert_status: AlertStatus,
204}
205
206/// Widget summary statistics
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct WidgetSummary {
209    /// Total count
210    pub total: f64,
211
212    /// Average value
213    pub average: f64,
214
215    /// Minimum value
216    pub minimum: f64,
217
218    /// Maximum value
219    pub maximum: f64,
220
221    /// Change from previous period
222    pub change_percent: Option<f64>,
223}
224
225/// Alert status for widgets
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
227pub enum AlertStatus {
228    Normal,
229    Warning,
230    Critical,
231    Unknown,
232}
233
234/// Complete dashboard configuration
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct Dashboard {
237    /// Dashboard identifier
238    pub id: String,
239
240    /// Dashboard title
241    pub title: String,
242
243    /// Dashboard description
244    pub description: Option<String>,
245
246    /// Dashboard configuration
247    pub config: DashboardConfig,
248
249    /// Widgets in this dashboard
250    pub widgets: Vec<DashboardWidget>,
251
252    /// Dashboard tags for organization
253    pub tags: Vec<String>,
254
255    /// Created timestamp
256    pub created_at: chrono::DateTime<chrono::Utc>,
257
258    /// Last modified timestamp
259    pub updated_at: chrono::DateTime<chrono::Utc>,
260}
261
262/// Dashboard manager
263pub struct DashboardManager {
264    config: DashboardConfig,
265    dashboards: HashMap<String, Dashboard>,
266}
267
268impl DashboardManager {
269    /// Create new dashboard manager
270    pub fn new(config: DashboardConfig) -> Self {
271        Self {
272            config,
273            dashboards: HashMap::new(),
274        }
275    }
276
277    /// Create a new dashboard
278    pub async fn create_dashboard(&mut self, dashboard: Dashboard) -> Result<(), AnalyticsError> {
279        self.dashboards.insert(dashboard.id.clone(), dashboard);
280        Ok(())
281    }
282
283    /// Get dashboard by ID
284    pub async fn get_dashboard(
285        &self,
286        dashboard_id: &str,
287    ) -> Result<Option<Dashboard>, AnalyticsError> {
288        Ok(self.dashboards.get(dashboard_id).cloned())
289    }
290
291    /// List all dashboards
292    pub async fn list_dashboards(&self) -> Result<Vec<Dashboard>, AnalyticsError> {
293        Ok(self.dashboards.values().cloned().collect())
294    }
295
296    /// Update dashboard
297    pub async fn update_dashboard(&mut self, dashboard: Dashboard) -> Result<(), AnalyticsError> {
298        let mut updated_dashboard = dashboard;
299        updated_dashboard.updated_at = chrono::Utc::now();
300        self.dashboards
301            .insert(updated_dashboard.id.clone(), updated_dashboard);
302        Ok(())
303    }
304
305    /// Delete dashboard
306    pub async fn delete_dashboard(&mut self, dashboard_id: &str) -> Result<bool, AnalyticsError> {
307        Ok(self.dashboards.remove(dashboard_id).is_some())
308    }
309
310    /// Get widget data
311    pub async fn get_widget_data(
312        &self,
313        widget: &DashboardWidget,
314    ) -> Result<WidgetData, AnalyticsError> {
315        let series = match &widget.data_source {
316            DataSource::RoleUsage { role_id, group_by } => {
317                self.get_role_usage_series(
318                    role_id.as_deref(),
319                    group_by.as_deref(),
320                    &widget.time_range,
321                )
322                .await?
323            }
324            DataSource::PermissionUsage {
325                permission_id,
326                group_by,
327            } => {
328                self.get_permission_usage_series(
329                    permission_id.as_deref(),
330                    group_by.as_deref(),
331                    &widget.time_range,
332                )
333                .await?
334            }
335            DataSource::Compliance { metric_type } => {
336                self.get_compliance_series(metric_type, &widget.time_range)
337                    .await?
338            }
339            DataSource::Performance { metric_type } => {
340                self.get_performance_series(metric_type, &widget.time_range)
341                    .await?
342            }
343            DataSource::EventCount {
344                event_type,
345                filters,
346            } => {
347                self.get_event_count_series(event_type.as_deref(), filters, &widget.time_range)
348                    .await?
349            }
350            DataSource::Custom { query, parameters } => {
351                self.get_custom_series(query, parameters, &widget.time_range)
352                    .await?
353            }
354        };
355
356        let summary = self.calculate_widget_summary(&series);
357        let alert_status = self.check_alert_status(&summary, &widget.alert_thresholds);
358
359        Ok(WidgetData {
360            widget_id: widget.id.clone(),
361            updated_at: chrono::Utc::now(),
362            series,
363            summary: Some(summary),
364            alert_status,
365        })
366    }
367
368    /// Create predefined RBAC overview dashboard
369    pub async fn create_rbac_overview_dashboard(&mut self) -> Result<String, AnalyticsError> {
370        let dashboard_id = uuid::Uuid::new_v4().to_string();
371
372        let dashboard = Dashboard {
373            id: dashboard_id.clone(),
374            title: "RBAC Overview".to_string(),
375            description: Some("Comprehensive RBAC system overview".to_string()),
376            config: self.config.clone(),
377            widgets: vec![
378                // Permission checks over time
379                DashboardWidget {
380                    id: "permission_checks_timeline".to_string(),
381                    title: "Permission Checks Over Time".to_string(),
382                    widget_type: WidgetType::LineChart,
383                    data_source: DataSource::EventCount {
384                        event_type: Some("PermissionCheck".to_string()),
385                        filters: HashMap::new(),
386                    },
387                    time_range: TimeRange::last_hours(24),
388                    layout: WidgetLayout {
389                        x: 0,
390                        y: 0,
391                        width: 6,
392                        height: 3,
393                    },
394                    refresh_interval: None,
395                    alert_thresholds: None,
396                },
397                // Role usage distribution
398                DashboardWidget {
399                    id: "role_usage_distribution".to_string(),
400                    title: "Role Usage Distribution".to_string(),
401                    widget_type: WidgetType::PieChart,
402                    data_source: DataSource::RoleUsage {
403                        role_id: None,
404                        group_by: Some("role_name".to_string()),
405                    },
406                    time_range: TimeRange::last_hours(24),
407                    layout: WidgetLayout {
408                        x: 6,
409                        y: 0,
410                        width: 6,
411                        height: 3,
412                    },
413                    refresh_interval: None,
414                    alert_thresholds: None,
415                },
416                // Compliance score
417                DashboardWidget {
418                    id: "compliance_score".to_string(),
419                    title: "Compliance Score".to_string(),
420                    widget_type: WidgetType::Gauge,
421                    data_source: DataSource::Compliance {
422                        metric_type: "overall_compliance".to_string(),
423                    },
424                    time_range: TimeRange::last_hours(24),
425                    layout: WidgetLayout {
426                        x: 0,
427                        y: 3,
428                        width: 3,
429                        height: 3,
430                    },
431                    refresh_interval: None,
432                    alert_thresholds: Some(AlertThresholds {
433                        warning: 85.0,
434                        critical: 70.0,
435                        comparison: ThresholdComparison::LessThan,
436                    }),
437                },
438                // Average response time
439                DashboardWidget {
440                    id: "avg_response_time".to_string(),
441                    title: "Average Response Time".to_string(),
442                    widget_type: WidgetType::MetricCard,
443                    data_source: DataSource::Performance {
444                        metric_type: "avg_permission_check_latency".to_string(),
445                    },
446                    time_range: TimeRange::last_hours(24),
447                    layout: WidgetLayout {
448                        x: 3,
449                        y: 3,
450                        width: 3,
451                        height: 3,
452                    },
453                    refresh_interval: None,
454                    alert_thresholds: Some(AlertThresholds {
455                        warning: 100.0,
456                        critical: 200.0,
457                        comparison: ThresholdComparison::GreaterThan,
458                    }),
459                },
460                // Top accessed resources
461                DashboardWidget {
462                    id: "top_resources".to_string(),
463                    title: "Top Accessed Resources".to_string(),
464                    widget_type: WidgetType::BarChart,
465                    data_source: DataSource::EventCount {
466                        event_type: Some("PermissionCheck".to_string()),
467                        filters: HashMap::from([("result".to_string(), "Success".to_string())]),
468                    },
469                    time_range: TimeRange::last_hours(24),
470                    layout: WidgetLayout {
471                        x: 6,
472                        y: 3,
473                        width: 6,
474                        height: 3,
475                    },
476                    refresh_interval: None,
477                    alert_thresholds: None,
478                },
479            ],
480            tags: vec!["rbac".to_string(), "overview".to_string()],
481            created_at: chrono::Utc::now(),
482            updated_at: chrono::Utc::now(),
483        };
484
485        self.create_dashboard(dashboard).await?;
486        Ok(dashboard_id)
487    }
488
489    // Private helper methods for data series generation
490    async fn get_role_usage_series(
491        &self,
492        _role_id: Option<&str>,
493        _group_by: Option<&str>,
494        _time_range: &TimeRange,
495    ) -> Result<Vec<ChartSeries>, AnalyticsError> {
496        // Implementation would query actual data
497        Ok(vec![
498            ChartSeries {
499                name: "Admin".to_string(),
500                data: vec![DataPoint {
501                    timestamp: None,
502                    label: Some("Admin".to_string()),
503                    value: 45.0,
504                    metadata: HashMap::new(),
505                }],
506                color: Some("#ff6b6b".to_string()),
507                series_type: None,
508            },
509            ChartSeries {
510                name: "User".to_string(),
511                data: vec![DataPoint {
512                    timestamp: None,
513                    label: Some("User".to_string()),
514                    value: 120.0,
515                    metadata: HashMap::new(),
516                }],
517                color: Some("#4ecdc4".to_string()),
518                series_type: None,
519            },
520        ])
521    }
522
523    async fn get_permission_usage_series(
524        &self,
525        _permission_id: Option<&str>,
526        _group_by: Option<&str>,
527        _time_range: &TimeRange,
528    ) -> Result<Vec<ChartSeries>, AnalyticsError> {
529        // Implementation would query actual data
530        Ok(vec![])
531    }
532
533    async fn get_compliance_series(
534        &self,
535        _metric_type: &str,
536        _time_range: &TimeRange,
537    ) -> Result<Vec<ChartSeries>, AnalyticsError> {
538        // Implementation would query actual data
539        Ok(vec![ChartSeries {
540            name: "Compliance Score".to_string(),
541            data: vec![DataPoint {
542                timestamp: None,
543                label: None,
544                value: 92.5,
545                metadata: HashMap::new(),
546            }],
547            color: Some("#45b7d1".to_string()),
548            series_type: None,
549        }])
550    }
551
552    async fn get_performance_series(
553        &self,
554        _metric_type: &str,
555        _time_range: &TimeRange,
556    ) -> Result<Vec<ChartSeries>, AnalyticsError> {
557        // Implementation would query actual data
558        Ok(vec![ChartSeries {
559            name: "Response Time".to_string(),
560            data: vec![DataPoint {
561                timestamp: None,
562                label: None,
563                value: 15.5,
564                metadata: HashMap::new(),
565            }],
566            color: Some("#96ceb4".to_string()),
567            series_type: None,
568        }])
569    }
570
571    async fn get_event_count_series(
572        &self,
573        _event_type: Option<&str>,
574        _filters: &HashMap<String, String>,
575        _time_range: &TimeRange,
576    ) -> Result<Vec<ChartSeries>, AnalyticsError> {
577        // Implementation would query actual data
578        Ok(vec![])
579    }
580
581    async fn get_custom_series(
582        &self,
583        _query: &str,
584        _parameters: &HashMap<String, String>,
585        _time_range: &TimeRange,
586    ) -> Result<Vec<ChartSeries>, AnalyticsError> {
587        // Implementation would execute custom query
588        Ok(vec![])
589    }
590
591    fn calculate_widget_summary(&self, series: &[ChartSeries]) -> WidgetSummary {
592        let all_values: Vec<f64> = series
593            .iter()
594            .flat_map(|s| s.data.iter().map(|d| d.value))
595            .collect();
596
597        let total = all_values.iter().sum();
598        let count = all_values.len() as f64;
599        let average = if count > 0.0 { total / count } else { 0.0 };
600        let minimum = all_values.iter().copied().fold(f64::INFINITY, f64::min);
601        let maximum = all_values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
602
603        WidgetSummary {
604            total,
605            average,
606            minimum: if minimum.is_infinite() { 0.0 } else { minimum },
607            maximum: if maximum.is_infinite() { 0.0 } else { maximum },
608            change_percent: None, // Would calculate from historical data
609        }
610    }
611
612    fn check_alert_status(
613        &self,
614        summary: &WidgetSummary,
615        thresholds: &Option<AlertThresholds>,
616    ) -> AlertStatus {
617        let Some(thresholds) = thresholds else {
618            return AlertStatus::Normal;
619        };
620
621        let value = summary.average; // Use average for threshold comparison
622
623        let exceeds_critical = match thresholds.comparison {
624            ThresholdComparison::GreaterThan => value > thresholds.critical,
625            ThresholdComparison::LessThan => value < thresholds.critical,
626            ThresholdComparison::Equals => (value - thresholds.critical).abs() < f64::EPSILON,
627            ThresholdComparison::NotEquals => (value - thresholds.critical).abs() > f64::EPSILON,
628        };
629
630        let exceeds_warning = match thresholds.comparison {
631            ThresholdComparison::GreaterThan => value > thresholds.warning,
632            ThresholdComparison::LessThan => value < thresholds.warning,
633            ThresholdComparison::Equals => (value - thresholds.warning).abs() < f64::EPSILON,
634            ThresholdComparison::NotEquals => (value - thresholds.warning).abs() > f64::EPSILON,
635        };
636
637        if exceeds_critical {
638            AlertStatus::Critical
639        } else if exceeds_warning {
640            AlertStatus::Warning
641        } else {
642            AlertStatus::Normal
643        }
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650
651    #[test]
652    fn test_dashboard_config_default() {
653        let config = DashboardConfig::default();
654        assert_eq!(config.refresh_interval_seconds, 30);
655        assert!(config.real_time_updates);
656        assert!(config.alerts_enabled);
657    }
658
659    #[tokio::test]
660    async fn test_dashboard_manager_creation() {
661        let config = DashboardConfig::default();
662        let manager = DashboardManager::new(config);
663        assert_eq!(manager.dashboards.len(), 0);
664    }
665
666    #[tokio::test]
667    async fn test_create_rbac_overview_dashboard() {
668        let config = DashboardConfig::default();
669        let mut manager = DashboardManager::new(config);
670
671        let dashboard_id = manager.create_rbac_overview_dashboard().await.unwrap();
672        assert!(!dashboard_id.is_empty());
673
674        let dashboard = manager.get_dashboard(&dashboard_id).await.unwrap().unwrap();
675        assert_eq!(dashboard.title, "RBAC Overview");
676        assert_eq!(dashboard.widgets.len(), 5);
677    }
678
679    #[test]
680    fn test_alert_status_checking() {
681        let config = DashboardConfig::default();
682        let manager = DashboardManager::new(config);
683
684        let summary = WidgetSummary {
685            total: 100.0,
686            average: 150.0,
687            minimum: 100.0,
688            maximum: 200.0,
689            change_percent: None,
690        };
691
692        let thresholds = AlertThresholds {
693            warning: 100.0,
694            critical: 200.0,
695            comparison: ThresholdComparison::GreaterThan,
696        };
697
698        let status = manager.check_alert_status(&summary, &Some(thresholds));
699        assert_eq!(status, AlertStatus::Warning);
700    }
701}
702
703