Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/mofa-monitoring/src/dashboard/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub trait AuthProvider: Send + Sync {
async fn validate(&self, token: &str) -> Result<AuthInfo, String>;
}

/// Allows all connections unconditionally (default).
/// Allows all connections unconditionally (development-only).
pub struct NoopAuthProvider;

#[async_trait]
Expand All @@ -33,7 +33,7 @@ impl AuthProvider for NoopAuthProvider {
async fn validate(&self, _token: &str) -> Result<AuthInfo, String> {
Ok(AuthInfo {
client_id: "anonymous".to_string(),
permissions: vec!["*".to_string()],
permissions: vec!["read:metrics".to_string()],
})
}
}
Expand Down Expand Up @@ -126,7 +126,9 @@ mod tests {

let result = provider.validate("anything").await;
assert!(result.is_ok());
assert_eq!(result.unwrap().client_id, "anonymous");
let auth_info = result.unwrap();
assert_eq!(auth_info.client_id, "anonymous");
assert_eq!(auth_info.permissions, vec!["read:metrics".to_string()]);
}

#[tokio::test]
Expand Down
3 changes: 3 additions & 0 deletions crates/mofa-monitoring/src/dashboard/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
//! - System resource usage
//! - REST API for integration
//! - WebSocket for live updates
//!
//! Authentication is required by default. For trusted local demos and tests,
//! explicitly opt out with [`DashboardConfig::with_require_auth(false)`].

mod api;
mod assets;
Expand Down
57 changes: 51 additions & 6 deletions crates/mofa-monitoring/src/dashboard/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ pub struct DashboardConfig {
pub ws_update_interval: Duration,
/// Enable request tracing
pub enable_tracing: bool,
/// WebSocket authentication provider (default: NoopAuthProvider)
/// Require a real auth provider before the dashboard server can start.
pub require_auth: bool,
/// WebSocket authentication provider.
///
/// Defaults to [`NoopAuthProvider`], but server startup requires either a
/// real provider via [`DashboardConfig::with_auth`] or an explicit
/// [`DashboardConfig::with_require_auth(false)`] opt-out.
pub auth_provider: Arc<dyn AuthProvider>,
/// Prometheus export configuration
pub prometheus_export_config: PrometheusExportConfig,
Expand All @@ -61,6 +67,7 @@ impl std::fmt::Debug for DashboardConfig {
.field("enable_cors", &self.enable_cors)
.field("ws_update_interval", &self.ws_update_interval)
.field("enable_tracing", &self.enable_tracing)
.field("require_auth", &self.require_auth)
.field("auth_enabled", &self.auth_provider.is_enabled())
.field("prometheus_export_config", &self.prometheus_export_config)
.finish()
Expand All @@ -76,6 +83,7 @@ impl Default for DashboardConfig {
metrics_config: MetricsConfig::default(),
ws_update_interval: Duration::from_secs(1),
enable_tracing: true,
require_auth: true,
auth_provider: Arc::new(NoopAuthProvider),
prometheus_export_config: PrometheusExportConfig::default(),
#[cfg(feature = "otlp-metrics")]
Expand Down Expand Up @@ -114,6 +122,15 @@ impl DashboardConfig {
self
}

/// Require or explicitly disable dashboard authentication.
///
/// `true` is the secure default. Set this to `false` only for trusted
/// local development or test environments.
pub fn with_require_auth(mut self, require_auth: bool) -> Self {
self.require_auth = require_auth;
self
}

/// Set the WebSocket authentication provider.
pub fn with_auth(mut self, provider: Arc<dyn AuthProvider>) -> Self {
self.auth_provider = provider;
Expand Down Expand Up @@ -160,6 +177,22 @@ pub struct DashboardServer {
impl DashboardServer {
/// Create a new dashboard server
pub fn new(config: DashboardConfig) -> Self {
if config.require_auth && !config.auth_provider.is_enabled() {
panic!(
"Dashboard authentication is required by default. Configure \
DashboardConfig::with_auth(...) or explicitly opt out with \
DashboardConfig::with_require_auth(false) for trusted local development."
);
}

if !config.require_auth && !config.auth_provider.is_enabled() {
warn!(
"Dashboard authentication is disabled; REST and WebSocket \
endpoints are publicly accessible. Only disable auth for \
trusted local development or tests."
);
}

let collector = Arc::new(MetricsCollector::new(config.metrics_config.clone()));
let prometheus_export_config = config.prometheus_export_config.clone();

Expand Down Expand Up @@ -442,9 +475,11 @@ async fn serve_prometheus_metrics(exporter: Arc<PrometheusExporter>) -> impl Int
)
}

/// Create a simple dashboard server with default configuration
/// Create a simple dashboard server for local development without auth.
pub fn create_dashboard(port: u16) -> DashboardServer {
let config = DashboardConfig::new().with_port(port);
let config = DashboardConfig::new()
.with_port(port)
.with_require_auth(false);
DashboardServer::new(config)
}

Expand All @@ -458,18 +493,21 @@ mod tests {
assert_eq!(config.port, 8080);
assert!(config.enable_cors);
assert!(config.enable_tracing);
assert!(config.require_auth);
}

#[test]
fn test_dashboard_config_builder() {
let config = DashboardConfig::new()
.with_host("127.0.0.1")
.with_port(3000)
.with_cors(false);
.with_cors(false)
.with_require_auth(false);

assert_eq!(config.host, "127.0.0.1");
assert_eq!(config.port, 3000);
assert!(!config.enable_cors);
assert!(!config.require_auth);
}

#[test]
Expand All @@ -482,9 +520,16 @@ mod tests {
assert_eq!(addr.port(), 8080);
}

#[tokio::test]
async fn test_dashboard_server_new() {
#[test]
#[should_panic(expected = "Dashboard authentication is required by default")]
fn test_dashboard_server_new_requires_auth_by_default() {
let config = DashboardConfig::default();
let _server = DashboardServer::new(config);
}

#[tokio::test]
async fn test_dashboard_server_new_allows_explicit_unauthenticated_mode() {
let config = DashboardConfig::default().with_require_auth(false);
let server = DashboardServer::new(config);

assert!(server.ws_handler.is_none());
Expand Down
3 changes: 2 additions & 1 deletion crates/mofa-monitoring/src/dashboard/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,8 @@ mod tests {

let config = crate::dashboard::server::DashboardConfig::new()
.with_host("127.0.0.1")
.with_port(0); // OS picks a free port
.with_port(0)
.with_require_auth(false); // OS picks a free port

let mut server = DashboardServer::new(config);
let router = server.build_router();
Expand Down
5 changes: 4 additions & 1 deletion crates/mofa-monitoring/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
//! # #[tokio::main]
//! # async fn main() {
//! let config = DashboardConfig::new()
//! .with_port(8080);
//! .with_port(8080)
//! // Explicit local-dev opt-out. Production deployments should configure
//! // a real auth provider via `.with_auth(...)`.
//! .with_require_auth(false);
//!
//! let server = DashboardServer::new(config);
//! // Start the server (this would block)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ use tower::ServiceExt;

#[tokio::test]
async fn metrics_route_returns_prometheus_payload() {
let mut server = DashboardServer::new(DashboardConfig::new()).with_prometheus_export_config(
PrometheusExportConfig::default().with_refresh_interval(Duration::from_millis(20)),
);
let mut server = DashboardServer::new(DashboardConfig::new().with_require_auth(false))
.with_prometheus_export_config(
PrometheusExportConfig::default().with_refresh_interval(Duration::from_millis(20)),
);

server
.collector()
Expand Down
34 changes: 18 additions & 16 deletions crates/mofa-monitoring/tests/metrics_export_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ use tower::ServiceExt;

#[tokio::test]
async fn metrics_route_returns_prometheus_payload_with_histograms() {
let mut server = DashboardServer::new(DashboardConfig::new()).with_prometheus_export_config(
PrometheusExportConfig::default()
.with_refresh_interval(Duration::from_millis(10))
.with_cardinality(CardinalityLimits::default()),
);
let mut server = DashboardServer::new(DashboardConfig::new().with_require_auth(false))
.with_prometheus_export_config(
PrometheusExportConfig::default()
.with_refresh_interval(Duration::from_millis(10))
.with_cardinality(CardinalityLimits::default()),
);

server
.collector()
Expand Down Expand Up @@ -77,17 +78,18 @@ async fn metrics_route_returns_prometheus_payload_with_histograms() {

#[tokio::test]
async fn metrics_route_applies_cardinality_overflow_bucket() {
let mut server = DashboardServer::new(DashboardConfig::new()).with_prometheus_export_config(
PrometheusExportConfig::default()
.with_refresh_interval(Duration::from_millis(10))
.with_cardinality(
CardinalityLimits::default()
.with_agent_id(1)
.with_workflow_id(100)
.with_plugin_or_tool(100)
.with_provider_model(50),
),
);
let mut server = DashboardServer::new(DashboardConfig::new().with_require_auth(false))
.with_prometheus_export_config(
PrometheusExportConfig::default()
.with_refresh_interval(Duration::from_millis(10))
.with_cardinality(
CardinalityLimits::default()
.with_agent_id(1)
.with_workflow_id(100)
.with_plugin_or_tool(100)
.with_provider_model(50),
),
);

for idx in 0..3 {
server
Expand Down
5 changes: 4 additions & 1 deletion crates/mofa-monitoring/tests/visual_debug_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ async fn test_debug_session_api_integration() {
recorder.end_session(session_id, "completed").await.unwrap();

// Step 2: Create dashboard server with the recorder
let config = DashboardConfig::new().with_port(18080).with_cors(true);
let config = DashboardConfig::new()
.with_port(18080)
.with_cors(true)
.with_require_auth(false);

let server = DashboardServer::new(config).with_session_recorder(recorder.clone());

Expand Down
1 change: 1 addition & 0 deletions docs/mofa-doc/src/examples/monitoring-observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.with_host("127.0.0.1")
.with_port(8080)
.with_cors(true)
.with_require_auth(false)
.with_ws_interval(Duration::from_secs(1));

// Create dashboard server
Expand Down
1 change: 1 addition & 0 deletions docs/mofa-doc/src/zh/examples/监控与可观测性.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.with_host("127.0.0.1")
.with_port(8080)
.with_cors(true)
.with_require_auth(false)
.with_ws_interval(Duration::from_secs(1));

// 创建仪表盘服务器
Expand Down
1 change: 1 addition & 0 deletions examples/monitoring_dashboard/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.with_host("127.0.0.1")
.with_port(8080)
.with_cors(true)
.with_require_auth(false)
.with_ws_interval(Duration::from_secs(1));

info!("📊 Starting dashboard server...");
Expand Down
Loading